@dcl/http-server
v2.4.0
Published
http server component
Readme
@dcl/http-server
forked from https://github.com/well-known-components/http-server
Server options
createServerComponent(components, options) accepts a Partial<IHttpServerOptions>
to tune the underlying Node server:
import { createServerComponent } from '@dcl/http-server'
const server = await createServerComponent(
{ config, logs },
{
// Reject request bodies larger than 1 MiB.
maxBodySize: 1024 * 1024,
// Abort a request whose headers + body don't arrive within 30s.
requestTimeout: 30_000,
maxHeadersCount: 100,
maxRequestsPerSocket: 1000,
cors: { origin: '*' }
}
)| Option | Maps to | Description |
| --- | --- | --- |
| maxBodySize | (enforced by this component) | Maximum size, in bytes, of an incoming request body. A request that declares a larger Content-Length is rejected with 413 Payload Too Large before the body is read; a body that omits or under-declares its length (e.g. chunked transfer-encoding) is capped with the same 413 while streaming. Unset means no limit. |
| cors | (CORS middleware) | CORS configuration applied as middleware. |
| keepAliveTimeout | server.keepAliveTimeout | Idle keep-alive socket timeout in ms (default 70000). |
| headersTimeout | server.headersTimeout | Time to receive the complete headers in ms (default 75000). |
| requestTimeout | server.requestTimeout | Time to receive the entire request in ms. 0 disables it. |
| maxHeadersCount | server.maxHeadersCount | Maximum number of request headers. 0 means unlimited. |
| maxRequestsPerSocket | server.maxRequestsPerSocket | Max requests served per keep-alive socket. Unset/0 means unlimited. |
A maxBodySize rejection produces a 413 that a handler can also surface itself: when
a handler reads an over-limit body inside the middleware chain (e.g. await ctx.request.json()),
the read rejects and the error middleware maps it to a 413 response.
maxBodySize must be a positive integer when provided — createServerComponent throws on
0, negative or fractional values (omit the option for "no limit"). When cors is also
configured, the up-front Content-Length rejection still carries the actual-response CORS
headers, so a cross-origin client can read the 413.
Reading the request body under a maxBodySize
The streaming limit is enforced as ctx.request.body is consumed, so it only turns into a 413
if your handler actually reads the body and propagates stream errors. Reading it with
await ctx.request.json() / .text() / .arrayBuffer() or for await (… of ctx.request.body)
does this for you — the read rejects with the 413 and the error middleware maps it to a response.
If you instead pipe ctx.request.body into another stream (e.g. a multipart parser like
busboy), remember that Readable.prototype.pipe does not forward source errors to the
destination. Attach an error handler to the adapted source, or a body-stream error — a client
abort or the maxBodySize limiter emitting its 413 — surfaces as an unhandled error (and can
crash the process):
import { Readable } from 'stream'
const body = Readable.fromWeb(ctx.request.body as any)
// Forward source errors (client abort, or the maxBodySize 413) so the parser rejects cleanly.
body.on('error', (err) => parser.destroy(err))
body.pipe(parser)Requests that declare a Content-Length over the limit — the usual case for multipart/form-data
uploads from browsers and fetch — are rejected up-front regardless of how the body is consumed.
Per-route body-size limits
maxBodySize on createServerComponent is server-wide. For a tighter limit on specific routes, use
createBodySizeLimitMiddleware(bytes) and mount it on those routes:
import { createBodySizeLimitMiddleware } from '@dcl/http-server'
// allow at most 4 KB on this endpoint
router.post('/v1/notes', createBodySizeLimitMiddleware(4096), notesHandler)It enforces the same dual check as the server-wide option: a request declaring a larger
Content-Length is rejected up front with 413 Payload Too Large, and a body that omits or
under-declares its length (e.g. chunked) is capped while streaming — the body read by downstream
handlers errors with a 413 once the limit is crossed. Either 413 sets Connection: close, so an
oversized or stalled client can't tie up the socket. bytes must be a positive integer or the
factory throws. It composes with the server-wide maxBodySize (the global cap still applies first).
Reading the client address
Every middleware context carries remoteAddress, the address of the socket the request arrived on:
server.use(async (ctx, next) => {
logger.info('request', { from: ctx.remoteAddress ?? 'unknown' })
return next()
})The value is captured synchronously while the request is built, so it survives a socket torn down
before the handler runs, and IPv4-mapped IPv6 is normalized (::ffff:127.0.0.1 becomes
127.0.0.1) so a client keys the same whether the listener is dual-stack or not. It lives on the
context rather than being looked up from the request, so it also survives middleware that replace
ctx.request — createBodySizeLimitMiddleware does exactly that.
It is undefined for requests that did not arrive over a socket (createTestServerComponent, or a
Request built directly). No placeholder is substituted, so a consumer that keys on the address has
to decide what absence means rather than silently bucketing every such request together.
This is the socket address, not the originating client. Behind a proxy or load balancer it is the proxy's address and is identical for every client. Anything that needs the real client — a rate limiter, an audit log — must read a trusted forwarding header first and fall back to this only when the server is directly exposed.
In tests, give the in-memory server a peer address with createTestServerComponent({ remoteAddress }),
or set one per request with setRemoteAddress(request, address) (which takes precedence):
const server = createTestServerComponent({ remoteAddress: '203.0.113.7' })Returning a native Response from a handler
Handlers return the structural IResponse (Node Readable/Buffer/string/JSON
bodies). A native Response (as returned by fetch, with a web ReadableStream
body) is not an IResponse — returning one via as any lets
response-transforming middleware (e.g. CORS) silently drop its status and body.
When you have a native Response (typically proxying an upstream fetch), adapt it
at the boundary with fromNativeResponse:
import { fromNativeResponse } from '@dcl/http-server'
router.get('/proxy/:id', async (ctx) => {
const upstream = await fetch(`${CONTENT_SERVER}/${ctx.params.id}`)
return fromNativeResponse(upstream)
})The body is streamed (not buffered), so proxying large responses stays
memory-safe. The passed Response is consumed by the returned stream — don't also
read it with .text()/.json().
Plain handlers are unaffected — keep returning the structural shape directly:
return { status: 200, body: { hello: 'world' } }