Files
bun.sh/test/regression/issue/25589-frame-size-server.js
Ciro Spaciari 2582e6f98e fix(http2): fix settings, window size handling, and dynamic header buffer allocation (#26119)
## 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>
2026-01-22 14:35:18 -08:00

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();