TIL: You can make HTTP requests without curl using Bash /dev/TCP
**Edit:** This has generated a nice discussion on Hacker News – check it out too!
I needed to check that one container could reach another over an internal Docker network: a plain `GET /health` against a service on a shared network. The obvious move is `curl http://service:8642/health`. But this app image was stripped right down, with no `curl` or `wget` and nothing else around that I could use to open a socket.
As it turns out, ~~`bash` can speak HTTP by itself~~`bash` can open a TCP socket, and you can write a small HTTP request to it by hand. Opening a connection to a host and port and writing the request needs nothing beyond the shell that’s already there:
bash
``` exec 3<>/dev/tcp/service/8642 printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3 cat <&3 ```
`service` here is just the hostname of whatever you’re talking to. It has to resolve and be reachable from wherever you run this, so it needs to be set up first: a container or service name on a Docker network you’ve configured, or any DNS name that resolves. Swap in your own host and port.
That prints the whole response: the status line, the headers, the blank line, and the body. To add a header, such as an `Authorization: Bearer` token, put another `\r\n`-terminated line before the blank line that ends the request:
bash
``` exec 3<>/dev/tcp/service/8642 printf 'GET /v1/models HTTP/1.1\r\nHost: service\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' "$API_KEY" >&3 cat <&3 ```
What caught me out the first time is that `/dev/tcp` isn’t a real device file. There’s no such path on disk; `ls /dev/tcp` finds nothing, and `cat /dev/tcp/...` from another shell just errors. It’s a redirection that `bash` handles internally. From the Bash manual:
`/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.
The names were picked because no real Unix has a `/dev/tcp` or `/dev/udp` hierarchy, so there’s nothing to collide with. Bash does the DNS lookup and the `connect(2)` for you, and `exec 3<>` hands the socket a file descriptor (`3`) you read from and write to like any other.
A few things worth knowing:
- This is not a real HTTP client. It does not parse HTTP properly, handle redirects, chunked responses, compression, retries, TLS, or any of the other things `curl` quietly does for you. It’s a quick connectivity and debugging trick.
- The `Connection: close` header matters. Without it the server keeps the connection open after it responds, which is the HTTP/1.1 default, and `cat <&3` then waits forever for bytes that never arrive. Asking the server to close means `cat` reaches EOF and returns. Wrapping the call in `timeout 6 bash -c '...'` covers you either way.
- There’s no TLS. `/dev/tcp` opens a raw socket, so this only works for plaintext HTTP. For `https` you’d need `openssl s_client`, and by then you may as well have the proper tools.
- This is a `bash` feature, not POSIX. `dash` (Debian’s `/bin/sh`) and `zsh` don’t have it, so a `#!/bin/sh` script can’t use it. Call `bash` directly.
- It’s a compile-time option, switched on when `bash` is built with `--enable-net-redirections`. Most mainstream builds enable it, and it worked without any fuss in the Debian-based image I was in, but Debian shipped it disabled for years, so on an old or very minimal system it’s worth checking first.
For day-to-day work `curl` is still the right tool. But inside a deliberately small container where you can’t install anything, this gets a quick check done without adding a package.