diff --git a/packages/connect-node/src/handler.ts b/packages/connect-node/src/handler.ts index d334b88ae..bcbf90b3f 100644 --- a/packages/connect-node/src/handler.ts +++ b/packages/connect-node/src/handler.ts @@ -99,7 +99,12 @@ export function createHandler( res: http.ServerResponse | http2.Http2ServerResponse ): void { if (method.kind == MethodKind.BiDiStreaming && req.httpVersionMajor !== 2) { - return void endWithHttpStatus(res, 505, "Version Not Supported"); + // Clients coded to expect full-duplex connections may hang if they've + // mistakenly negotiated HTTP/1.1. To unblock them, we must close the + // underlying TCP connection. + return void endWithHttpStatus(res, 505, "Version Not Supported", { + Connection: "close", + }); } if (req.method !== "POST") { // The gRPC-HTTP2, gRPC-Web, and Connect protocols are all POST-only. diff --git a/packages/connect-node/src/private/io.ts b/packages/connect-node/src/private/io.ts index 00d8c166e..83da8e88a 100644 --- a/packages/connect-node/src/private/io.ts +++ b/packages/connect-node/src/private/io.ts @@ -19,7 +19,10 @@ import { assert } from "./assert.js"; import type { ReadableStreamReadResultLike } from "../lib.dom.streams.js"; import { Code, ConnectError, EnvelopedMessage } from "@bufbuild/connect-core"; import type { JsonValue } from "@bufbuild/protobuf"; -import { nodeHeaderToWebHeader } from "./web-header-to-node-headers.js"; +import { + nodeHeaderToWebHeader, + webHeaderToNodeHeaders, +} from "./web-header-to-node-headers.js"; export function jsonParse(bytes: Uint8Array): JsonValue { const buf = bytes instanceof Buffer ? bytes : Buffer.from(bytes); @@ -197,13 +200,17 @@ export function readToEnd(stream: stream.Readable): Promise { export async function endWithHttpStatus( res: http.ServerResponse | http2.Http2ServerResponse, statusCode: number, - statusMessage: string + statusMessage: string, + header?: HeadersInit ): Promise { + const headers: http.OutgoingHttpHeaders | undefined = header + ? webHeaderToNodeHeaders(header) + : undefined; if ("createPushResponse" in res) { // this is a HTTP/2 response, which does not support status messages - res.writeHead(statusCode); + res.writeHead(statusCode, headers); } else { - res.writeHead(statusCode, statusMessage); + res.writeHead(statusCode, statusMessage, headers); } await end(res); }