diff --git a/src/string_bytes.cc b/src/string_bytes.cc index 1d4ee3a81803b2..81ea5f2c342380 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -472,7 +472,6 @@ Maybe StringBytes::StorageSize(Isolate* isolate, break; case HEX: - CHECK(view.length() % 2 == 0 && "invalid hex string length"); data_size = view.length() / 2; break; diff --git a/test/parallel/test-http-odd-hex-write.js b/test/parallel/test-http-odd-hex-write.js new file mode 100644 index 00000000000000..a0e44377ece77b --- /dev/null +++ b/test/parallel/test-http-odd-hex-write.js @@ -0,0 +1,91 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const http = require('node:http'); +const net = require('node:net'); + +// Regression test for https://github.com/nodejs/node/issues/45150 +// Writing an odd-length hex string to a stream that batches writes via +// Writev (e.g. HTTP requests that are automatically corked) used to +// fatal-assert in StringBytes::StorageSize. The trailing incomplete nibble +// should be silently dropped, consistent with non-Writev paths. + +// Test 1: HTTP POST with a single odd-length hex write. +// "1" has no complete bytes in hex encoding, so the request body is empty. +{ + const server = http.createServer(common.mustCall((req, res) => { + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', common.mustCall(() => { + assert.strictEqual(Buffer.concat(chunks).length, 0); + res.end(); + server.close(); + })); + })); + + server.listen(0, common.mustCall(() => { + const req = http.request({ + port: server.address().port, + method: 'POST', + }, common.mustCall((res) => { + res.resume(); + })); + req.write('1', 'hex'); + req.end(); + })); +} + +// Test 2: HTTP POST with cork/uncork and mixed odd-length hex writes. +// "ff1" (3 hex chars) decodes to 1 byte (0xff); the trailing "1" nibble is +// dropped. "1" (1 hex char) decodes to 0 bytes. +{ + const server = http.createServer(common.mustCall((req, res) => { + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', common.mustCall(() => { + assert.deepStrictEqual(Buffer.concat(chunks), Buffer.from([0xff])); + res.end(); + server.close(); + })); + })); + + server.listen(0, common.mustCall(() => { + const req = http.request({ + port: server.address().port, + method: 'POST', + }, common.mustCall((res) => { + res.resume(); + })); + req.cork(); + req.write('ff1', 'hex'); + req.write('1', 'hex'); + req.uncork(); + req.end(); + })); +} + +// Test 3: net socket with cork/uncork and an odd-length hex write. +// Exercises the Writev path directly at the net layer. +{ + const server = net.createServer(common.mustCall((socket) => { + const chunks = []; + socket.on('data', (chunk) => chunks.push(chunk)); + socket.on('end', common.mustCall(() => { + assert.deepStrictEqual(Buffer.concat(chunks), Buffer.from([0xff])); + server.close(); + })); + socket.resume(); + })); + + server.listen(0, common.mustCall(() => { + const conn = net.createConnection(server.address().port); + conn.on('connect', common.mustCall(() => { + conn.cork(); + conn.write('ff', 'hex'); + conn.write('1', 'hex'); + conn.uncork(); + conn.end(); + })); + })); +}