I learned this neat Bash trick today where you can make a raw HTTP request using the
/dev/tcp
file descriptor without using tools like curl
or wget
. This came in handy
while writing a health check script that needed to make a TCP request to a service.
The following script opens a TCP connection and makes a simple GET request to example.com
:
#!/bin/bash
# Open a TCP connection to example.com on port 80 and assign file descriptor 3
# The exec command keeps /dev/fd/3 open throughout the lifetime of the script
# 3<> enables bidirectional read-write
exec 3<>/dev/tcp/example.com/80
# Send the HTTP GET request to the server
# >& redirects stdout to /dev/fd/3
echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n" >&3
# Read and print the server's response
# <& redirects the output of /dev/fd/3 to cat
cat <&3
# Close the file descriptor, terminating the TCP connection
exec 3>&-
Running this will print the response from the site to your console.
The snippet first opens a TCP connection to example.com
on port 80 and assigns file
descriptor 3 to manage this connection. The exec
ensures that the file descriptor 3
remains open for the duration of the script, allowing multiple read and write operations
without needing to reopen the connection each time. Using a file descriptor makes the code
cleaner. Without it, we’d need to redirect input and output directly to
/dev/tcp/example.com/80
for each read and write operation, making the script more
cumbersome and harder to read.
Then we send an HTTP GET request to the server by echoing the request to file descriptor 3.
The server’s response is read and printed using cat <&3
, which reads from the file
descriptor and prints the output to the console. Finally, the script closes the connection
by terminating file descriptor 3 with exec 3>&-
.
This is a Bash-specific trick and won’t work in other shells like Zsh or Fish. It also allows you to open UDP connections in the same manner. The Bash manpage explains the usage like this:
/dev/tcp/host/port
If host is a valid hostname or Internet address, and port
is an integer port number or service name, bash attempts
to open the corresponding TCP socket.
/dev/udp/host/port
If host is a valid hostname or Internet address, and port
is an integer port number or service name, bash attempts
to open the corresponding UDP socket.
I used this to write the following health check script. I didn’t want to install curl
in a
sidecar container that just runs a single health check process, keeping things simpler.
#!/bin/bash
# Enable bash strict mode
set -euo pipefail
# Constants
readonly HOST="example.com"
readonly PORT=80
readonly HEALTH_PATH="/"
# Open a TCP connection to the specified host and port
exec 3<>"/dev/tcp/${HOST}/${PORT}"
# Send the HTTP GET request to the server
echo -e \
"GET ${HEALTH_PATH} HTTP/1.1\r\nHost: ${HOST}\r\nConnection: close\r\n\r\n" \
>&3
# Read the HTTP status from the server's response
read -r HTTP_RESPONSE <&3
HTTP_STATUS=$(
echo "${HTTP_RESPONSE}" \
| grep -o "HTTP/1.1 [0-9]*" \
| cut -d ' ' -f 2
)
if [[ "${HTTP_STATUS}" == "200" ]]; then
echo "Service is healthy."
exit 0
else
echo "Service is not healthy. HTTP status: ${HTTP_STATUS}"
exit 1
fi
# Close the file descriptor, terminating the TCP connection
exec 3>&-
The script makes a GET request to the service and checks that the HTTP status from the raw response is 200. If not, it exits with a non-zero status.
Note that the script will fail if your service returns a 301 redirect code. Plus, you need
to make raw textual HTTP requests, which can become cumbersome if you need to do anything
beyond a simple GET call. At that point, you’re better off using curl
.
Recent posts
- Shades of testing HTTP requests in Python
- Taming parametrize with pytest.param
- Log context propagation in Python ASGI apps
- Please don't hijack my Python root logger
- The *nix install command
- Here-doc headache
- The sane pull request
- I kind of like rebasing
- Protobuffed contracts
- TypeIs does what I thought TypeGuard would do in Python