A straightforward way to send data for http.ServerResponse.
It's like res.send, but:
- It accepts any kind of value (number, string, object, stream, etc).
- It leaves a response that already answered alone, rather than throwing at you.
- It determines
Content-Typefrom the data: the type for values, the first bytes for streams. - It optionally sets the status code as second argument.
- It tears down both ends when a stream fails or the client disconnects.
- It proxies an HTTP response with
proxy, keeping the upstream status and the headers that describe the body. - It's small (~110 LOC, one dependency).
$ npm install send-http --saveIt requires Node.js >= 24.
const send = require('send-http')
const http = require('http')
const got = require('got')
http.createServer((req, res) => {
/* send with no body */
send(res, 200)
/* send a string */
send(res, 200, 'foo')
/* send an object */
send(res, 200, { foo: 'bar' })
/* send a number */
send(res, 200, 1234)
/* send a buffer */
send(res, 200, Buffer.from('hello world'))
/* send a stream */
send(res, 200, got.stream('https.//example.com'))
})When the body is a stream, the Content-Type is detected from its first bytes via @kikobeats/set-content-type. An already set Content-Type is respected, and an unrecognized payload leaves it unset. proxy is different: when upstream sends Content-Type and it crosses the allowlist, that value is forwarded and the body is not sampled.
When a stream fails before anything reached the client, the response is closed without the error: an upstream that never answered is not a failure of the response itself. Once bytes are on the wire they cannot be retracted, so the response is destroyed with the failure and the client sees a reset rather than a truncated body that looks complete.
Use sendStream to decide what the client gets instead:
const { sendStream } = require('send-http')
http.createServer((req, res) => {
sendStream(res, 200, got.stream('https://example.com'), {
onError: (error, res) => {
res.statusCode = error.code === 'ETIMEDOUT' ? 504 : 502
res.end()
}
})
})onError runs on the stream, before the response is torn down, so it can still write a reply. Write one and the response is left alone; write nothing and it is closed for you. Once the headers are out there is nothing left to answer with: send returns the response untouched rather than throwing ERR_HTTP_HEADERS_SENT at you from inside a listener, and the client sees the failure as a reset.
send takes the same options as a fourth argument and forwards them when the body is a stream, so send(res, 200, stream, { onError }) is sendStream(res, 200, stream, { onError }).
Relaying an HTTP response is not the same as sending a stream: status and body-descriptor headers come from the upstream once it answers.
const { proxy } = require('send-http')
http.createServer((req, res) => {
proxy(res, got.stream('https://example.com/video.mp4'), {
onError: (error, res) => send(res, 502, { error: error.message })
})
})onError follows the same rules as sendStream above.
headers is an allowlist of lowercase names that may cross; nothing else does. It defaults to STREAM_ALLOWED_HEADERS:
const { STREAM_ALLOWED_HEADERS } = require('send-http')
proxy(res, upstream, { headers: [...STREAM_ALLOWED_HEADERS, 'etag'] })content-length is not in the default list: upstreams often declare a wrong length while still sending the full body, so the proxied response uses chunked transfer instead. Re-adding it takes that back, and a length the upstream got wrong truncates the response.
proxy accepts either a stream that emits response later, or an IncomingMessage that already answered:
proxy(res, got.stream(url))
http.get(url, upstream => proxy(res, upstream))got.stream decompresses by default. Decoding hands over a different representation of the body, so content-encoding, content-length, content-range and etag from that hop are dropped rather than mislabel the bytes being piped. That holds however they got on the list, the default one or a custom one. Pass { decoded: false } to relay the compressed representation instead (same as piping an IncomingMessage), and all four cross with the bytes they describe.
When content-type crosses, the first byte reaches the client as soon as the upstream produces it. If it does not cross, Content-Type is sniffed from the first bytes like sendStream.
Teardown matches streams (see above): the upstream is destroyed when the client leaves, including before the upstream has answered.
Customizes the write of a buffered body (streams go through sendStream, and an empty body ends the response without a write to customize). The hook runs after the Content-Type and Content-Length are set, and receives the body as a Buffer whatever it was given:
const send = require('send-http').create((res, data) => {
if (data.length > 6291456) {
throw new Error('Payload size is over 6mb')
}
return res.end(data)
})The predicate behind the dispatch, exported for reusing the same rule:
const { isStream } = require('send-http')
isStream(got.stream('https://example.com')) // => true
isStream({}) // => falseThe guard behind the no-op: false once the headers went out or the response ended, which is when writing to it would throw. Use it to skip work whose only purpose was the answer:
const { canAnswer } = require('send-http')
if (canAnswer(res)) send(res, 500, { error: 'upstream failed' })send already checks it, so an unconditional call is safe too: it returns the response untouched rather than throwing.
send-http © Kiko Beats, released under the MIT License.
Authored and maintained by Kiko Beats with help from contributors.
kikobeats.com · GitHub Kiko Beats · X @Kikobeats