mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
## Summary This PR fixes multiple HTTP/2 protocol compliance issues that were causing stream errors with various HTTP/2 clients (Fauna, gRPC/Connect, etc.). fixes https://github.com/oven-sh/bun/issues/12544 fixes https://github.com/oven-sh/bun/issues/25589 ### Key Fixes **Window Size and Settings Handling** - Fix initial stream window size to use `DEFAULT_WINDOW_SIZE` until `SETTINGS_ACK` is received - Per RFC 7540 Section 6.5.1: The sender can only rely on settings being applied AFTER receiving `SETTINGS_ACK` - Properly adjust existing stream windows when `INITIAL_WINDOW_SIZE` setting changes (RFC 7540 Section 6.9.2) **Header List Size Enforcement** - Implement `maxHeaderListSize` checking per RFC 7540 Section 6.5.2 - Track cumulative header list size using HPACK entry overhead (32 bytes per RFC 7541 Section 4.1) - Reject streams with `ENHANCE_YOUR_CALM` when header list exceeds configured limit **Custom Settings Support** - Add validation for `customSettings` option (up to 10 custom settings, matching Node.js `MAX_ADDITIONAL_SETTINGS`) - Validate setting IDs are in range `[0, 0xFFFF]` per RFC 7540 - Validate setting values are in range `[0, 2^32-1]` **Settings Validation Improvements** - Use float comparison for settings validation to handle large values correctly (was using `toInt32()` which truncates) - Use proper `HTTP2_INVALID_SETTING_VALUE_RangeError` error codes for Node.js compatibility **BufferFallbackAllocator** - New allocator that tries a provided buffer first, falls back to heap: - Similar to `std.heap.stackFallback` but accepts external buffer slice - Used with `shared_request_buffer` (16KB threadlocal) for common cases - Falls back to `bun.default_allocator` for large headers ## Test Plan - [x] `bun bd` compiles successfully - [x] Node.js HTTP/2 tests pass: `bun bd test/js/node/test/parallel/test-http2-connect.js` - [x] New regression tests for frame size issues: `bun bd test test/regression/issue/25589.test.ts` - [x] HTTP/2 continuation tests: `bun bd test test/js/node/http2/node-http2-continuation.test.ts` --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
163 lines
4.8 KiB
JavaScript
163 lines
4.8 KiB
JavaScript
/**
|
|
* Node.js gRPC server fixture for testing HTTP/2 FRAME_SIZE_ERROR
|
|
* This server configures large frame sizes and can return large responses
|
|
* to test Bun's HTTP/2 client handling of large frames.
|
|
*/
|
|
|
|
const grpc = require("@grpc/grpc-js");
|
|
const loader = require("@grpc/proto-loader");
|
|
const { join } = require("path");
|
|
const { readFileSync } = require("fs");
|
|
|
|
const protoLoaderOptions = {
|
|
keepCase: true,
|
|
longs: String,
|
|
enums: String,
|
|
defaults: true,
|
|
oneofs: true,
|
|
};
|
|
|
|
function loadProtoFile(file) {
|
|
const packageDefinition = loader.loadSync(file, protoLoaderOptions);
|
|
return grpc.loadPackageDefinition(packageDefinition);
|
|
}
|
|
|
|
// Use the existing proto file from grpc-js tests
|
|
const protoFile = join(__dirname, "../../js/third_party/grpc-js/fixtures/echo_service.proto");
|
|
const echoService = loadProtoFile(protoFile).EchoService;
|
|
|
|
// TLS certificates from grpc-js fixtures
|
|
const ca = readFileSync(join(__dirname, "../../js/third_party/grpc-js/fixtures/ca.pem"));
|
|
const key = readFileSync(join(__dirname, "../../js/third_party/grpc-js/fixtures/server1.key"));
|
|
const cert = readFileSync(join(__dirname, "../../js/third_party/grpc-js/fixtures/server1.pem"));
|
|
|
|
// Service implementation that can return large responses
|
|
const serviceImpl = {
|
|
echo: (call, callback) => {
|
|
const request = call.request;
|
|
const metadata = call.metadata;
|
|
|
|
// Check if client wants large response headers
|
|
const largeHeaders = metadata.get("x-large-headers");
|
|
if (largeHeaders.length > 0) {
|
|
const responseMetadata = new grpc.Metadata();
|
|
// Add many headers to exceed 16KB
|
|
const headerCount = parseInt(largeHeaders[0]) || 100;
|
|
for (let i = 0; i < headerCount; i++) {
|
|
responseMetadata.add(`x-header-${i}`, "A".repeat(200));
|
|
}
|
|
call.sendMetadata(responseMetadata);
|
|
}
|
|
|
|
// Check if client wants large response value
|
|
const largeResponse = metadata.get("x-large-response");
|
|
if (largeResponse.length > 0) {
|
|
const size = parseInt(largeResponse[0]) || 32768; // Default 32KB
|
|
callback(null, { value: "X".repeat(size), value2: 0 });
|
|
return;
|
|
}
|
|
|
|
// Check if client wants large trailers
|
|
const largeTrailers = metadata.get("x-large-trailers");
|
|
if (largeTrailers.length > 0) {
|
|
const size = parseInt(largeTrailers[0]) || 20000;
|
|
const trailerMetadata = new grpc.Metadata();
|
|
trailerMetadata.add("grpc-status-details-bin", Buffer.from("X".repeat(size)));
|
|
call.sendMetadata(call.metadata);
|
|
callback(null, { value: request.value || "echo", value2: request.value2 || 0 }, trailerMetadata);
|
|
return;
|
|
}
|
|
|
|
// Default: echo back the request
|
|
if (call.metadata) {
|
|
call.sendMetadata(call.metadata);
|
|
}
|
|
callback(null, request);
|
|
},
|
|
|
|
echoClientStream: (call, callback) => {
|
|
let lastMessage = { value: "", value2: 0 };
|
|
call.on("data", message => {
|
|
lastMessage = message;
|
|
});
|
|
call.on("end", () => {
|
|
callback(null, lastMessage);
|
|
});
|
|
},
|
|
|
|
echoServerStream: call => {
|
|
const metadata = call.metadata;
|
|
const largeResponse = metadata.get("x-large-response");
|
|
|
|
if (largeResponse.length > 0) {
|
|
const size = parseInt(largeResponse[0]) || 32768;
|
|
// Send a single large response
|
|
call.write({ value: "X".repeat(size), value2: 0 });
|
|
} else {
|
|
// Echo the request
|
|
call.write(call.request);
|
|
}
|
|
call.end();
|
|
},
|
|
|
|
echoBidiStream: call => {
|
|
call.on("data", message => {
|
|
call.write(message);
|
|
});
|
|
call.on("end", () => {
|
|
call.end();
|
|
});
|
|
},
|
|
};
|
|
|
|
function main() {
|
|
// Parse server options from environment
|
|
const optionsJson = process.env.GRPC_SERVER_OPTIONS;
|
|
let serverOptions = {
|
|
// Default: allow very large messages
|
|
"grpc.max_send_message_length": -1,
|
|
"grpc.max_receive_message_length": -1,
|
|
};
|
|
|
|
if (optionsJson) {
|
|
try {
|
|
serverOptions = { ...serverOptions, ...JSON.parse(optionsJson) };
|
|
} catch (e) {
|
|
console.error("Failed to parse GRPC_SERVER_OPTIONS:", e);
|
|
}
|
|
}
|
|
|
|
const server = new grpc.Server(serverOptions);
|
|
|
|
// Handle shutdown
|
|
process.stdin.on("data", data => {
|
|
const cmd = data.toString().trim();
|
|
if (cmd === "shutdown") {
|
|
server.tryShutdown(() => {
|
|
process.exit(0);
|
|
});
|
|
}
|
|
});
|
|
|
|
server.addService(echoService.service, serviceImpl);
|
|
|
|
const useTLS = process.env.GRPC_TEST_USE_TLS === "true";
|
|
let credentials;
|
|
if (useTLS) {
|
|
credentials = grpc.ServerCredentials.createSsl(ca, [{ private_key: key, cert_chain: cert }]);
|
|
} else {
|
|
credentials = grpc.ServerCredentials.createInsecure();
|
|
}
|
|
|
|
server.bindAsync("localhost:0", credentials, (err, port) => {
|
|
if (err) {
|
|
console.error("Failed to bind server:", err);
|
|
process.exit(1);
|
|
}
|
|
// Output the address for the test to connect to
|
|
process.stdout.write(JSON.stringify({ address: "localhost", family: "IPv4", port }));
|
|
});
|
|
}
|
|
|
|
main();
|