Skip to content
File

Blob: src/workerd/api/node/tests/http-outgoing-nodejs-test.js

javascript466 lines
1// Copyright (c) 2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import http from 'node:http';
5import { throws, strictEqual, ok } from 'node:assert';
6import { httpServerHandler } from 'cloudflare:node';
7import { mock } from 'node:test';
8import stream from 'node:stream';
9 
10export const checkPortsSetCorrectly = {
11 test(_ctrl, env) {
12 const keys = [
13 'SIDECAR_HOSTNAME',
14 'FINISH_WRITABLE_PORT',
15 'WRITABLE_FINISHED_PORT',
16 'PROPERTIES_PORT',
17 ];
18 for (const key of keys) {
19 strictEqual(typeof env[key], 'string');
20 ok(env[key].length > 0);
21 }
22 },
23};
24 
25// Test is taken from test/parallel/test-http-outgoing-destroy.js
26export const testHttpOutgoingDestroy = {
27 async test() {
28 const { promise, resolve, reject } = Promise.withResolvers();
29 const msg = new http.OutgoingMessage();
30 strictEqual(msg.destroyed, false);
31 msg.destroy();
32 strictEqual(msg.destroyed, true);
33 msg.write('asd', (err) => {
34 strictEqual(err.code, 'ERR_STREAM_DESTROYED');
35 resolve();
36 });
37 msg.on('error', reject);
38 await promise;
39 },
40};
41 
42// Tests are taken from test/parallel/test-http-outgoing-finish-writable.js
43export const testHttpOutgoingFinishWritable = {
44 async test(_ctrl, env) {
45 const clientRequest = http.request({
46 hostname: env.SIDECAR_HOSTNAME,
47 port: env.FINISH_WRITABLE_PORT,
48 method: 'GET',
49 path: '/',
50 });
51 
52 strictEqual(clientRequest.writable, true);
53 clientRequest.end();
54 
55 // Writable is still true when close
56 // THIS IS LEGACY, we cannot change it
57 // unless we break error detection
58 strictEqual(clientRequest.writable, true);
59 },
60};
61 
62// Tests are taken from test/parallel/test-http-outgoing-properties.js
63export const testHttpOutgoingProperties = {
64 async test(_ctrl, env) {
65 {
66 const msg = new http.OutgoingMessage();
67 msg._implicitHeader = function () {};
68 strictEqual(msg.writableLength, 0);
69 // TODO(soon): Implement _write() method of OutgoingMessage
70 // msg.write('asd');
71 // strictEqual(msg.writableLength, 3);
72 }
73 
74 {
75 const req = http.request({
76 hostname: env.SIDECAR_HOSTNAME,
77 port: env.PROPERTIES_PORT,
78 method: 'GET',
79 path: '/',
80 });
81 
82 strictEqual(req.path, '/');
83 strictEqual(req.method, 'GET');
84 strictEqual(req.host, env.SIDECAR_HOSTNAME);
85 strictEqual(req.protocol, 'http:');
86 req.end();
87 }
88 },
89};
90 
91// This is a modified test which is taken from
92// https://github.com/nodejs/node/blob/462c74181d8e15e74bc5a25d55290d93bd7edf65/test/parallel/test-http-outgoing-proto.js#L47
93export const testHttpOutgoingProto = {
94 async test() {
95 throws(
96 () => {
97 const outgoingMessage = new http.OutgoingMessage();
98 outgoingMessage.setHeader();
99 },
100 {
101 code: 'ERR_INVALID_HTTP_TOKEN',
102 name: 'TypeError',
103 message: 'Header name must be a valid HTTP token ["undefined"]',
104 }
105 );
106 
107 throws(
108 () => {
109 const outgoingMessage = new http.OutgoingMessage();
110 outgoingMessage.setHeader('test');
111 },
112 {
113 code: 'ERR_HTTP_INVALID_HEADER_VALUE',
114 name: 'TypeError',
115 message: 'Invalid value "undefined" for header "test"',
116 }
117 );
118 
119 throws(
120 () => {
121 const outgoingMessage = new http.OutgoingMessage();
122 outgoingMessage.setHeader(404);
123 },
124 {
125 code: 'ERR_INVALID_HTTP_TOKEN',
126 name: 'TypeError',
127 message: 'Header name must be a valid HTTP token ["404"]',
128 }
129 );
130 
131 throws(
132 () => {
133 const outgoingMessage = new http.OutgoingMessage();
134 outgoingMessage.setHeader('200', 'あ');
135 },
136 {
137 code: 'ERR_INVALID_CHAR',
138 name: 'TypeError',
139 message: 'Invalid character in header content ["200"]',
140 }
141 );
142 
143 {
144 const outgoingMessage = new http.OutgoingMessage();
145 strictEqual(outgoingMessage.destroyed, false);
146 outgoingMessage.destroy();
147 strictEqual(outgoingMessage.destroyed, true);
148 }
149 },
150};
151 
152// Tests are taken from test/parallel/test-http-outgoing-renderHeaders.js
153export const testHttpOutgoingRenderHeaders = {
154 async test() {
155 const outgoingMessage = new http.OutgoingMessage();
156 outgoingMessage._header = {};
157 throws(() => outgoingMessage._renderHeaders(), {
158 code: 'ERR_HTTP_HEADERS_SENT',
159 name: 'Error',
160 message: 'Cannot render headers after they are sent to the client',
161 });
162 },
163};
164 
165// Tests are taken from test/parallel/test-http-outgoing-writableFinished.js
166export const testHttpOutgoingWritableFinished = {
167 async test(_ctrl, env) {
168 const { promise, resolve } = Promise.withResolvers();
169 const clientRequest = http.request({
170 hostname: env.SIDECAR_HOSTNAME,
171 port: env.WRITABLE_FINISHED_PORT,
172 method: 'GET',
173 path: '/',
174 });
175 
176 strictEqual(clientRequest.writableFinished, false);
177 clientRequest
178 .on('finish', () => {
179 strictEqual(clientRequest.writableFinished, true);
180 resolve();
181 })
182 .end();
183 strictEqual(clientRequest.writableFinished, false);
184 await promise;
185 },
186};
187 
188// Test is taken from test/parallel/test-http-outgoing-destroyed.js
189export const testHttpOutgoingDestroyed = {
190 async test(_ctrl, env) {
191 {
192 const { promise, resolve } = Promise.withResolvers();
193 const errorFn = mock.fn();
194 await using server = http.createServer((req, res) => {
195 strictEqual(res.closed, false);
196 req.pipe(res);
197 res.on('error', errorFn);
198 res.on('close', () => {
199 strictEqual(res.closed, true);
200 res.end('asd');
201 process.nextTick(resolve);
202 });
203 });
204 
205 server.listen(8080);
206 
207 await env.SERVICE.fetch('https://cloudflare.com', {
208 method: 'PUT',
209 body: 'asd',
210 });
211 await promise;
212 strictEqual(errorFn.mock.callCount(), 0);
213 }
214 
215 {
216 await using server = http.createServer((req, res) => {
217 strictEqual(res.closed, false);
218 res.end();
219 res.destroy();
220 // Make sure not to emit 'error' after .destroy().
221 res.end('asd');
222 strictEqual(res.errored, undefined);
223 });
224 
225 server.listen(8080);
226 
227 await env.SERVICE.fetch('https://cloudflare.com');
228 }
229 },
230};
231 
232// Test is taken from test/parallel/test-http-outgoing-end-multiple.js
233export const testHttpOutgoingEndMultiple = {
234 async test(_ctrl, env) {
235 const { promise, resolve } = Promise.withResolvers();
236 const onWriteAfterEndError = mock.fn((err) => {
237 strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END');
238 });
239 const resEndFn = mock.fn();
240 await using server = http.createServer(function (req, res) {
241 res.end('testing ended state', resEndFn);
242 strictEqual(res.writableCorked, 0);
243 res.end((err) => {
244 strictEqual(err.code, 'ERR_STREAM_ALREADY_FINISHED');
245 });
246 strictEqual(res.writableCorked, 0);
247 res.end('end', onWriteAfterEndError);
248 strictEqual(res.writableCorked, 0);
249 res.on('error', onWriteAfterEndError);
250 res.on('finish', () => {
251 res.end((err) => {
252 strictEqual(err.code, 'ERR_STREAM_ALREADY_FINISHED');
253 resolve();
254 });
255 });
256 });
257 
258 server.listen(8080);
259 
260 await env.SERVICE.fetch('https://cloudflare.com/');
261 await promise;
262 
263 strictEqual(onWriteAfterEndError.mock.callCount(), 2);
264 strictEqual(resEndFn.mock.callCount(), 1);
265 },
266};
267 
268// Test is taken from test/parallel/test-http-outgoing-write-types.js
269export const testHttpOutgoingWriteTypes = {
270 async test(_ctrl, env) {
271 await using httpServer = http.createServer(function (req, res) {
272 throws(
273 () => {
274 res.write(['Throws.']);
275 },
276 { code: 'ERR_INVALID_ARG_TYPE' }
277 );
278 // should not throw
279 res.write('1a2b3c');
280 // should not throw
281 res.write(new Uint8Array(1024));
282 // should not throw
283 res.write(Buffer.from('1'.repeat(1024)));
284 res.end();
285 });
286 
287 httpServer.listen(8080);
288 
289 await env.SERVICE.fetch('https://cloudflare.com');
290 },
291};
292 
293// Test is taken from test/parallel/test-http-outgoing-buffer.js
294export const testHttpOutgoingBuffer = {
295 async test() {
296 const msg = new http.OutgoingMessage();
297 msg._implicitHeader = function () {};
298 
299 // Writes should be buffered until highwatermark
300 // even when no socket is assigned.
301 
302 strictEqual(msg.write('asd'), true);
303 while (msg.write('asd'));
304 const highwatermark = msg.writableHighWaterMark || 65535;
305 ok(msg.outputSize >= highwatermark);
306 },
307};
308 
309// Test is taken from test/parallel/test-http-outgoing-finished.js
310export const testHttpOutgoingFinished = {
311 async test(_ctrl, env) {
312 const { promise: _promise, resolve } = Promise.withResolvers();
313 await using server = http.createServer(function (req, res) {
314 let closed = false;
315 res
316 .on('close', () => {
317 closed = true;
318 stream.finished(res, resolve);
319 })
320 .end();
321 stream.finished(res, () => {
322 strictEqual(closed, true);
323 });
324 });
325 
326 server.listen(8080);
327 
328 await env.SERVICE.fetch('https://cloudflare.com');
329 },
330};
331 
332// Test is taken from test/parallel/test-http-outgoing-end-types.js
333export const testHttpOutgoingEndTypes = {
334 async test(_ctrl, env) {
335 await using httpServer = http.createServer(function (req, res) {
336 throws(
337 () => {
338 res.end(['Throws.']);
339 },
340 {
341 code: 'ERR_INVALID_ARG_TYPE',
342 }
343 );
344 res.end();
345 });
346 
347 httpServer.listen(8080);
348 
349 await env.SERVICE.fetch('https://cloudflare.com');
350 },
351};
352 
353// Test is taken from test/parallel/test-http-outgoing-first-chunk-singlebyte-encoding.js
354export const testHttpOutgoingFirstChunkSingleByteEncoding = {
355 async test(_ctrl, env) {
356 // TODO(soon): Add utf-16 to this list when it's supported.
357 for (const enc of ['utf8', 'latin1', 'UTF-8']) {
358 await using server = http.createServer((req, res) => {
359 res.setHeader('content-type', `text/plain; charset=${enc}`);
360 res.write('helloworld', enc);
361 res.end();
362 });
363 
364 server.listen(8080);
365 
366 const res = await env.SERVICE.fetch('http://cloudflare.com', {
367 headers: {
368 'content-type': `text/plain; charset=${enc}`,
369 },
370 });
371 strictEqual(res.status, 200);
372 strictEqual(
373 res.headers.get('content-type'),
374 `text/plain; charset=${enc}`
375 );
376 const buf = await res.arrayBuffer();
377 strictEqual(Buffer.from(buf, enc).toString(), 'helloworld');
378 }
379 },
380};
381 
382// Test client request lines don't cause parsing errors
383export const testHttpOutgoingClientRequestLine = {
384 async test() {
385 const msg = new http.OutgoingMessage();
386 
387 // Client request line (POST /path HTTP/1.1) should not throw parsing errors
388 msg._header =
389 'POST /w/?token=BQYAAABQFGo%2b___snip___t90WZ%2bjnA%3d HTTP/1.1\r\n\r\n';
390 
391 const result = msg._send('test data');
392 strictEqual(typeof result, 'boolean');
393 },
394};
395 
396// Test server response lines emit _headersSent event
397export const testHttpOutgoingServerResponseLine = {
398 async test() {
399 const msg = new http.OutgoingMessage();
400 let headersSentEvent = null;
401 
402 msg.on('_headersSent', (event) => {
403 headersSentEvent = event;
404 });
405 
406 // Server response status line should be parsed and emit _headersSent event
407 msg._header = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n';
408 
409 const result = msg._send('test data');
410 
411 strictEqual(typeof result, 'boolean');
412 ok(headersSentEvent, '_headersSent event should be emitted');
413 strictEqual(headersSentEvent.statusCode, 200);
414 strictEqual(headersSentEvent.statusMessage, 'OK');
415 strictEqual(
416 headersSentEvent.headers.get('content-type'),
417 'application/json'
418 );
419 },
420};
421 
422// Test different HTTP versions work
423export const testHttpOutgoingDifferentVersions = {
424 async test() {
425 for (const version of ['HTTP/1.0', 'HTTP/1.1', 'HTTP/2.0']) {
426 const msg = new http.OutgoingMessage();
427 let headersSentEvent = null;
428 msg.on('_headersSent', (event) => {
429 headersSentEvent = event;
430 });
431 msg._header = `${version} 404 Not Found\r\n\r\n`;
432 msg._send('test');
433 ok(headersSentEvent, `${version} should work`);
434 strictEqual(headersSentEvent.statusCode, 404);
435 strictEqual(headersSentEvent.statusMessage, 'Not Found');
436 }
437 },
438};
439 
440export default httpServerHandler({ port: 8080 });
441 
442// Relevant Node.js tests
443//
444// - [x] test/parallel/test-http-outgoing-buffer.js
445// - [x] test/parallel/test-http-outgoing-destroy.js
446// - [x] test/parallel/test-http-outgoing-destroyed.js
447// - [x] test/parallel/test-http-outgoing-end-multiple.js
448// - [x] test/parallel/test-http-outgoing-end-types.js
449// - [x] test/parallel/test-http-outgoing-first-chunk-singlebyte-encoding.js
450// - [x] test/parallel/test-http-outgoing-finished.js
451// - [x] test/parallel/test-http-outgoing-finish-writable.js
452// - [x] test/parallel/test-http-outgoing-properties.js
453// - [x] test/parallel/test-http-outgoing-proto.js
454// - [x] test/parallel/test-http-outgoing-renderHeaders.js
455// - [x] test/parallel/test-http-outgoing-writableFinished.js
456// - [x] test/parallel/test-http-outgoing-write-types.js
457 
458// The following tests is not relevant to us:
459//
460// - [ ] test/parallel/test-http-outgoing-end-cork.js
461// - [ ] test/parallel/test-http-outgoing-finish.js
462// - [ ] test/parallel/test-http-outgoing-message-capture-rejection.js
463// - [ ] test/parallel/test-http-outgoing-message-inheritance.js
464// - [ ] test/parallel/test-http-outgoing-message-write-callback.js
465// - [ ] test/parallel/test-http-outgoing-settimeout.js