From 15271feb16b2a413dbfe92c3e9467ce0f2c6a28a Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Mon, 4 Mar 2024 10:39:55 +0530 Subject: [PATCH 01/15] feat(ext/node): implement Http2ServerRequest working --- ext/node/polyfills/http2.ts | 557 +++++++++++++++++++++++--- ext/node/polyfills/internal/errors.ts | 10 + ext/node/polyfills/net.ts | 7 +- 3 files changed, 516 insertions(+), 58 deletions(-) diff --git a/ext/node/polyfills/http2.ts b/ext/node/polyfills/http2.ts index d86148e4e2d064..8453e44ce4263c 100644 --- a/ext/node/polyfills/http2.ts +++ b/ext/node/polyfills/http2.ts @@ -4,7 +4,7 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials -import { core } from "ext:core/mod.js"; +import { core, primordials } from "ext:core/mod.js"; import { op_http2_client_get_response, op_http2_client_get_response_body_chunk, @@ -18,6 +18,7 @@ import { } from "ext:core/ops"; import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; +import { Readable } from "node:stream"; import { EventEmitter } from "node:events"; import { Buffer } from "node:buffer"; import { connect as netConnect, Server, Socket, TCP } from "node:net"; @@ -44,16 +45,24 @@ import { ERR_HTTP2_INVALID_PSEUDOHEADER, ERR_HTTP2_INVALID_SESSION, ERR_HTTP2_INVALID_STREAM, + ERR_HTTP2_NO_SOCKET_MANIPULATION, ERR_HTTP2_SESSION_ERROR, ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_STREAM_ERROR, + ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, ERR_HTTP2_TRAILERS_ALREADY_SENT, ERR_HTTP2_TRAILERS_NOT_READY, ERR_HTTP2_UNSUPPORTED_PROTOCOL, + ERR_INVALID_ARG_VALUE, ERR_INVALID_HTTP_TOKEN, ERR_SOCKET_CLOSED, } from "ext:deno_node/internal/errors.ts"; import { _checkIsHttpToken } from "ext:deno_node/_http_common.ts"; +const { + StringPrototypeTrim, + FunctionPrototypeBind, + ReflectGetPrototypeOf, +} = primordials; const kSession = Symbol("session"); const kAlpnProtocol = Symbol("alpnProtocol"); @@ -85,6 +94,9 @@ const STREAM_FLAGS_HEAD_REQUEST = 0x8; const STREAM_FLAGS_ABORTED = 0x10; const STREAM_FLAGS_HAS_TRAILERS = 0x20; +// Maximum number of allowed additional settings +const MAX_ADDITIONAL_SETTINGS = 10; + const SESSION_FLAGS_PENDING = 0x0; const SESSION_FLAGS_READY = 0x1; const SESSION_FLAGS_CLOSED = 0x2; @@ -93,7 +105,7 @@ const SESSION_FLAGS_DESTROYED = 0x4; const ENCODER = new TextEncoder(); type Http2Headers = Record; -const debugHttp2Enabled = false; +const debugHttp2Enabled = true; function debugHttp2(...args) { if (debugHttp2Enabled) { console.log(...args); @@ -331,6 +343,9 @@ function closeSession(session: Http2Session, code?: number, error?: Error) { export class ServerHttp2Session extends Http2Session { constructor() { super(constants.NGHTTP2_SESSION_SERVER, {}); + this.on("stream", (stream, headers) => { + console.log(stream, headers); + }); } altsvc( @@ -1308,6 +1323,12 @@ export class ServerHttp2Stream extends Http2Stream { headers: Http2Headers, options: Record, ) { + debugHttp2( + "ServerHttp2Stream.respond() headers:", + headers, + "options:", + options, + ); this.#headersSent = true; const response: ResponseInit = {}; if (headers) { @@ -1344,6 +1365,111 @@ export class ServerHttp2Stream extends Http2Stream { } } +const kOptions = Symbol("options"); + +function setupCompat(ev) { + if (ev === "request") { + this.removeListener("newListener", setupCompat); + this.on( + "stream", + FunctionPrototypeBind( + onServerStream, + this, + this[kOptions].Http2ServerRequest, + this[kOptions].Http2ServerResponse, + ), + ); + } +} + +function onServerStream( + ServerRequest, + ServerResponse, + stream, + headers, + flags, + rawHeaders, +) { + console.log("handling on stream event"); + const server = this; + const request = new ServerRequest(stream, headers, undefined, rawHeaders); + const response = new ServerResponse(stream); + + // Check for the CONNECT method + const method = headers[constants.HTTP2_HEADER_METHOD]; + if (method === "CONNECT") { + if (!server.emit("connect", request, response)) { + response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED; + response.end(); + } + return; + } + + // Check for Expectations + if (headers.expect !== undefined) { + if (headers.expect === "100-continue") { + if (server.listenerCount("checkContinue")) { + server.emit("checkContinue", request, response); + } else { + response.writeContinue(); + server.emit("request", request, response); + } + } else if (server.listenerCount("checkExpectation")) { + server.emit("checkExpectation", request, response); + } else { + response.statusCode = constants.HTTP_STATUS_EXPECTATION_FAILED; + response.end(); + } + return; + } + + server.emit("request", request, response); +} + +function initializeOptions(options) { + // assertIsObject(options, 'options'); + options = { ...options }; + // assertIsObject(options.settings, 'options.settings'); + options.settings = { ...options.settings }; + + // assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings'); + if (options.remoteCustomSettings) { + options.remoteCustomSettings = [...options.remoteCustomSettings]; + if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) { + throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); + } + } + + // if (options.maxSessionInvalidFrames !== undefined) + // validateUint32(options.maxSessionInvalidFrames, 'maxSessionInvalidFrames'); + + // if (options.maxSessionRejectedStreams !== undefined) { + // validateUint32( + // options.maxSessionRejectedStreams, + // 'maxSessionRejectedStreams', + // ); + // } + + if (options.unknownProtocolTimeout !== undefined) { + // validateUint32(options.unknownProtocolTimeout, 'unknownProtocolTimeout'); + } else { + // TODO(danbev): is this a good default value? + options.unknownProtocolTimeout = 10000; + } + + // Used only with allowHTTP1 + // options.Http1IncomingMessage = options.Http1IncomingMessage || + // http.IncomingMessage; + // options.Http1ServerResponse = options.Http1ServerResponse || + // http.ServerResponse; + + options.Http2ServerRequest = options.Http2ServerRequest || + Http2ServerRequest; + options.Http2ServerResponse = options.Http2ServerResponse || + Http2ServerResponse; + return options; +} + export class Http2Server extends Server { #options: Record = {}; #abortController; @@ -1354,11 +1480,16 @@ export class Http2Server extends Server { options: Record, requestListener: () => unknown, ) { + options = initializeOptions(options); super(options); + this[kOptions] = options; this.#abortController = new AbortController(); + this.on("newListener", setupCompat); + this.on( "connection", (conn: Deno.Conn) => { + console.log("got connection: ", conn); try { const session = new ServerHttp2Session(); this.emit("session", session); @@ -1366,7 +1497,9 @@ export class Http2Server extends Server { conn, this.#abortController.signal, async (req: Request) => { + console.log("got request", req); try { + console.log("inside try block"); const controllerDeferred = Promise.withResolvers< ReadableStreamDefaultController >(); @@ -1375,12 +1508,14 @@ export class Http2Server extends Server { controllerDeferred.resolve(controller); }, }); + console.log("before headers"); const headers: Http2Headers = {}; for (const [name, value] of req.headers) { headers[name] = value; } headers[constants.HTTP2_HEADER_PATH] = new URL(req.url).pathname; + console.log("headers: ", headers); const stream = new ServerHttp2Stream( session, Promise.resolve(headers), @@ -1388,9 +1523,13 @@ export class Http2Server extends Server { req.body, body, ); + console.log("stream created successfully"); session.emit("stream", stream, headers); this.emit("stream", stream, headers); - return await stream._deferred.promise; + console.log("stream emitted"); + let res = await stream._deferred.promise; + console.log("res: ", res); + return res; } catch (e) { console.log(">>> Error in serveHttpOnConnection", e); } @@ -1406,10 +1545,6 @@ export class Http2Server extends Server { } }, ); - this.on( - "newListener", - (event) => console.log(`Event in newListener: ${event}`), - ); this.#options = options; if (typeof requestListener === "function") { this.on("request", requestListener); @@ -1907,89 +2042,401 @@ export function getUnpackedSettings( export const sensitiveHeaders = Symbol("nodejs.http2.sensitiveHeaders"); -export class Http2ServerRequest { - constructor() { +const kBeginSend = Symbol("begin-send"); +const kStream = Symbol("stream"); +const kResponse = Symbol("response"); +const kHeaders = Symbol("headers"); +const kRawHeaders = Symbol("rawHeaders"); +const kSocket = Symbol("socket"); +const kTrailers = Symbol("trailers"); +const kRawTrailers = Symbol("rawTrailers"); +const kSetHeader = Symbol("setHeader"); +const kAppendHeader = Symbol("appendHeader"); +const kAborted = Symbol("aborted"); +const kProxySocket = Symbol("proxySocket"); +const kRequest = Symbol("request"); + +let statusMessageWarned = false; +let statusConnectionHeaderWarned = false; + +const proxySocketHandler = { + has(stream, prop) { + const ref = stream.session !== undefined ? stream.session[kSocket] : stream; + return (prop in stream) || (prop in ref); + }, + + get(stream, prop) { + switch (prop) { + case "on": + case "once": + case "end": + case "emit": + case "destroy": + return FunctionPrototypeBind(stream[prop], stream); + case "writable": + case "destroyed": + return stream[prop]; + case "readable": { + if (stream.destroyed) { + return false; + } + const request = stream[kRequest]; + return request ? request.readable : stream.readable; + } + case "setTimeout": { + const session = stream.session; + if (session !== undefined) { + return FunctionPrototypeBind(session.setTimeout, session); + } + return FunctionPrototypeBind(stream.setTimeout, stream); + } + case "write": + case "read": + case "pause": + case "resume": + throw new ERR_HTTP2_NO_SOCKET_MANIPULATION(); + default: { + const ref = stream.session !== undefined + ? stream.session[kSocket] + : stream; + const value = ref[prop]; + return typeof value === "function" + ? FunctionPrototypeBind(value, ref) + : value; + } + } + }, + getPrototypeOf(stream) { + if (stream.session !== undefined) { + return ReflectGetPrototypeOf(stream.session[kSocket]); + } + return ReflectGetPrototypeOf(stream); + }, + set(stream, prop, value) { + switch (prop) { + case "writable": + case "readable": + case "destroyed": + case "on": + case "once": + case "end": + case "emit": + case "destroy": + stream[prop] = value; + return true; + case "setTimeout": { + const session = stream.session; + if (session !== undefined) { + session.setTimeout = value; + } else { + stream.setTimeout = value; + } + return true; + } + case "write": + case "read": + case "pause": + case "resume": + throw new ERR_HTTP2_NO_SOCKET_MANIPULATION(); + default: { + const ref = stream.session !== undefined + ? stream.session[kSocket] + : stream; + ref[prop] = value; + return true; + } + } + }, +}; + +function onStreamCloseRequest() { + const req = this[kRequest]; + + if (req === undefined) { + return; } - get aborted(): boolean { - notImplemented("Http2ServerRequest.aborted"); - return false; + const state = req[kState]; + state.closed = true; + + req.push(null); + // If the user didn't interact with incoming data and didn't pipe it, + // dump it for compatibility with http1 + if (!state.didRead && !req._readableState.resumeScheduled) { + req.resume(); } - get authority(): string { - notImplemented("Http2ServerRequest.authority"); - return ""; + this[kProxySocket] = null; + this[kRequest] = undefined; + + req.emit("close"); +} + +function onStreamTimeout(kind) { + return function onStreamTimeout() { + const obj = this[kind]; + obj.emit("timeout"); + }; +} + +class Http2ServerRequest extends Readable { + readableEnded = false; + + constructor(stream, headers, options, rawHeaders) { + super({ autoDestroy: false, ...options }); + this[kState] = { + closed: false, + didRead: false, + }; + // Headers in HTTP/1 are not initialized using Object.create(null) which, + // although preferable, would simply break too much code. Ergo header + // initialization using Object.create(null) in HTTP/2 is intentional. + this[kHeaders] = headers; + this[kRawHeaders] = rawHeaders; + this[kTrailers] = {}; + this[kRawTrailers] = []; + this[kStream] = stream; + this[kAborted] = false; + stream[kProxySocket] = null; + stream[kRequest] = this; + + // Pause the stream.. + stream.on("trailers", onStreamTrailers); + stream.on("end", onStreamEnd); + stream.on("error", onStreamError); + stream.on("aborted", onStreamAbortedRequest); + stream.on("close", onStreamCloseRequest); + stream.on("timeout", onStreamTimeout(kRequest)); + this.on("pause", onRequestPause); + this.on("resume", onRequestResume); } - get complete(): boolean { - notImplemented("Http2ServerRequest.complete"); - return false; + get aborted() { + return this[kAborted]; } - get connection(): Socket /*| TlsSocket*/ { - notImplemented("Http2ServerRequest.connection"); - return {}; + get complete() { + return this[kAborted] || + this.readableEnded || + this[kState].closed || + this[kStream].destroyed; } - destroy(_error: Error) { - notImplemented("Http2ServerRequest.destroy"); + get stream() { + return this[kStream]; } - get headers(): Record { - notImplemented("Http2ServerRequest.headers"); - return {}; + get headers() { + return this[kHeaders]; } - get httpVersion(): string { - notImplemented("Http2ServerRequest.httpVersion"); - return ""; + get rawHeaders() { + return this[kRawHeaders]; } - get method(): string { - notImplemented("Http2ServerRequest.method"); - return ""; + get trailers() { + return this[kTrailers]; } - get rawHeaders(): string[] { - notImplemented("Http2ServerRequest.rawHeaders"); - return []; + get rawTrailers() { + return this[kRawTrailers]; } - get rawTrailers(): string[] { - notImplemented("Http2ServerRequest.rawTrailers"); - return []; + get httpVersionMajor() { + return 2; } - get scheme(): string { - notImplemented("Http2ServerRequest.scheme"); - return ""; + get httpVersionMinor() { + return 0; } - setTimeout(msecs: number, callback?: () => unknown) { - this.stream.setTimeout(callback, msecs); + get httpVersion() { + return "2.0"; } - get socket(): Socket /*| TlsSocket*/ { - notImplemented("Http2ServerRequest.socket"); - return {}; + get socket() { + const stream = this[kStream]; + const proxySocket = stream[kProxySocket]; + if (proxySocket === null) { + return stream[kProxySocket] = new Proxy(stream, proxySocketHandler); + } + return proxySocket; } - get stream(): Http2Stream { - notImplemented("Http2ServerRequest.stream"); - return new Http2Stream(); + get connection() { + return this.socket; } - get trailers(): Record { - notImplemented("Http2ServerRequest.trailers"); - return {}; + // _read(nread) { + // const state = this[kState]; + // assert(!state.closed); + // if (!state.didRead) { + // state.didRead = true; + // this[kStream].on("data", onStreamData); + // } else { + // process.nextTick(resumeStream, this[kStream]); + // } + // } + + get method() { + return this[kHeaders][constants.HTTP2_HEADER_METHOD]; } - get url(): string { - notImplemented("Http2ServerRequest.url"); - return ""; + set method(method) { + // validateString(method, "method"); + if (StringPrototypeTrim(method) === "") { + throw new ERR_INVALID_ARG_VALUE("method", method); + } + + this[kHeaders][constants.HTTP2_HEADER_METHOD] = method; + } + + get authority() { + return getAuthority(this[kHeaders]); + } + + get scheme() { + return this[kHeaders][constants.HTTP2_HEADER_SCHEME]; + } + + get url() { + return this[kHeaders][constants.HTTP2_HEADER_PATH]; + } + + set url(url) { + this[kHeaders][constants.HTTP2_HEADER_PATH] = url; + } + + setTimeout(msecs, callback) { + if (!this[kState].closed) { + this[kStream].setTimeout(msecs, callback); + } + return this; + } +} + +function onStreamEnd() { + // Cause the request stream to end as well. + const request = this[kRequest]; + if (request !== undefined) { + this[kRequest].push(null); } } +function onStreamError(error) { + // This is purposefully left blank + // + // errors in compatibility mode are + // not forwarded to the request + // and response objects. +} + +function onRequestPause() { + this[kStream].pause(); +} + +function onRequestResume() { + this[kStream].resume(); +} + +function onStreamDrain() { + const response = this[kResponse]; + if (response !== undefined) { + response.emit("drain"); + } +} + +function onStreamAbortedRequest() { + const request = this[kRequest]; + if (request !== undefined && request[kState].closed === false) { + request[kAborted] = true; + request.emit("aborted"); + } +} + +// export class Http2ServerRequest extends Readable { +// constructor() { +// } + +// get aborted(): boolean { +// notImplemented("Http2ServerRequest.aborted"); +// return false; +// } + +// get authority(): string { +// notImplemented("Http2ServerRequest.authority"); +// return ""; +// } + +// get complete(): boolean { +// notImplemented("Http2ServerRequest.complete"); +// return false; +// } + +// get connection(): Socket /*| TlsSocket*/ { +// notImplemented("Http2ServerRequest.connection"); +// return {}; +// } + +// destroy(_error: Error) { +// notImplemented("Http2ServerRequest.destroy"); +// } + +// get headers(): Record { +// notImplemented("Http2ServerRequest.headers"); +// return {}; +// } + +// get httpVersion(): string { +// notImplemented("Http2ServerRequest.httpVersion"); +// return ""; +// } + +// get method(): string { +// notImplemented("Http2ServerRequest.method"); +// return ""; +// } + +// get rawHeaders(): string[] { +// notImplemented("Http2ServerRequest.rawHeaders"); +// return []; +// } + +// get rawTrailers(): string[] { +// notImplemented("Http2ServerRequest.rawTrailers"); +// return []; +// } + +// get scheme(): string { +// notImplemented("Http2ServerRequest.scheme"); +// return ""; +// } + +// setTimeout(msecs: number, callback?: () => unknown) { +// this.stream.setTimeout(callback, msecs); +// } + +// get socket(): Socket /*| TlsSocket*/ { +// notImplemented("Http2ServerRequest.socket"); +// return {}; +// } + +// get stream(): Http2Stream { +// notImplemented("Http2ServerRequest.stream"); +// return new Http2Stream(); +// } + +// get trailers(): Record { +// notImplemented("Http2ServerRequest.trailers"); +// return {}; +// } + +// get url(): string { +// notImplemented("Http2ServerRequest.url"); +// return ""; +// } +// } + export class Http2ServerResponse { constructor() { } diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts index 5d5946d464c740..5a6446ae8f7bd7 100644 --- a/ext/node/polyfills/internal/errors.ts +++ b/ext/node/polyfills/internal/errors.ts @@ -2248,6 +2248,16 @@ export class ERR_FALSY_VALUE_REJECTION extends NodeError { this.reason = reason; } } + +export class ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS extends NodeError { + constructor() { + super( + "ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS", + "Number of custom settings exceeds MAX_ADDITIONAL_SETTINGS", + ); + } +} + export class ERR_HTTP2_INVALID_SETTING_VALUE extends NodeRangeError { actual: unknown; min?: number; diff --git a/ext/node/polyfills/net.ts b/ext/node/polyfills/net.ts index 66b7735d959728..6f81d69b823d82 100644 --- a/ext/node/polyfills/net.ts +++ b/ext/node/polyfills/net.ts @@ -106,9 +106,10 @@ import type { BufferEncoding } from "ext:deno_node/_global.d.ts"; import type { Abortable } from "ext:deno_node/_events.d.ts"; import { channel } from "node:diagnostics_channel"; -let debug = debuglog("net", (fn) => { - debug = fn; -}); +// let debug = debuglog("net", (fn) => { +// debug = fn; +// }); +let debug = console.log; const kLastWriteQueueSize = Symbol("lastWriteQueueSize"); const kSetNoDelay = Symbol("kSetNoDelay"); From 133b3901d6999a84603cf7d6e6a6b4f3f8766cc8 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Tue, 5 Mar 2024 15:55:16 +0530 Subject: [PATCH 02/15] wip: need to get addTrailers to work first --- a.mjs | 14 + ext/node/polyfills/http2.ts | 747 ++++++++++++++++++++++++++++++++---- main.mjs | 7 + tests/unit/serve_test.ts | 2 +- 4 files changed, 685 insertions(+), 85 deletions(-) create mode 100644 a.mjs create mode 100644 main.mjs diff --git a/a.mjs b/a.mjs new file mode 100644 index 00000000000000..53adf114b9ee70 --- /dev/null +++ b/a.mjs @@ -0,0 +1,14 @@ +import http2 from "node:http2"; + +const server = http2.createServer((req, res) => { + console.log("handler called"); + console.log(req); + res.setHeader("Content-Type", "text/html"); + res.setHeader("X-Foo", "bar"); + res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); + res.write("Hello, World!"); + console.log(res); + res.end(); +}); + +server.listen(8000); diff --git a/ext/node/polyfills/http2.ts b/ext/node/polyfills/http2.ts index 8453e44ce4263c..3ac1c152a67163 100644 --- a/ext/node/polyfills/http2.ts +++ b/ext/node/polyfills/http2.ts @@ -21,6 +21,8 @@ import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; import { Readable } from "node:stream"; import { EventEmitter } from "node:events"; import { Buffer } from "node:buffer"; +import { emitWarning } from "node:process"; +import Stream from "node:stream"; import { connect as netConnect, Server, Socket, TCP } from "node:net"; import { connect as tlsConnect } from "node:tls"; import { TypedArray } from "ext:deno_node/internal/util/types.ts"; @@ -42,11 +44,14 @@ import { ERR_HTTP2_CONNECT_PATH, ERR_HTTP2_CONNECT_SCHEME, ERR_HTTP2_GOAWAY_SESSION, + ERR_HTTP2_HEADERS_SENT, + ERR_HTTP2_INFO_STATUS_NOT_ALLOWED, ERR_HTTP2_INVALID_PSEUDOHEADER, ERR_HTTP2_INVALID_SESSION, ERR_HTTP2_INVALID_STREAM, ERR_HTTP2_NO_SOCKET_MANIPULATION, ERR_HTTP2_SESSION_ERROR, + ERR_HTTP2_STATUS_INVALID, ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_STREAM_ERROR, ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, @@ -56,12 +61,19 @@ import { ERR_INVALID_ARG_VALUE, ERR_INVALID_HTTP_TOKEN, ERR_SOCKET_CLOSED, + ERR_STREAM_WRITE_AFTER_END, } from "ext:deno_node/internal/errors.ts"; import { _checkIsHttpToken } from "ext:deno_node/_http_common.ts"; const { StringPrototypeTrim, FunctionPrototypeBind, + ObjectKeys, ReflectGetPrototypeOf, + ObjectAssign, + StringPrototypeToLowerCase, + ReflectApply, + ArrayIsArray, + ObjectPrototypeHasOwnProperty, } = primordials; const kSession = Symbol("session"); @@ -712,8 +724,11 @@ export class Http2Stream extends EventEmitter { return {}; } - sendTrailers(_headers: Record) { - addTrailers(this._response, [["grpc-status", "0"], ["grpc-message", "OK"]]); + sendTrailers(headers: Record) { + console.log("sendTrailers(headers): ", headers); + + // addTrailers(this._response, [["grpc-status", "0"], ["grpc-message", "OK"]]); + addTrailers(this._response, Object.entries(headers)); } } @@ -2056,9 +2071,6 @@ const kAborted = Symbol("aborted"); const kProxySocket = Symbol("proxySocket"); const kRequest = Symbol("request"); -let statusMessageWarned = false; -let statusConnectionHeaderWarned = false; - const proxySocketHandler = { has(stream, prop) { const ref = stream.session !== undefined ? stream.session[kSocket] : stream; @@ -2274,7 +2286,7 @@ class Http2ServerRequest extends Readable { // state.didRead = true; // this[kStream].on("data", onStreamData); // } else { - // process.nextTick(resumeStream, this[kStream]); + // nextTick(resumeStream, this[kStream]); // } // } @@ -2437,136 +2449,703 @@ function onStreamAbortedRequest() { // } // } -export class Http2ServerResponse { - constructor() { +function onStreamTrailersReady() { + this.sendTrailers(this[kResponse][kTrailers]); +} + +function onStreamCloseResponse() { + const res = this[kResponse]; + + if (res === undefined) { + return; } - addTrailers(_headers: Record) { - notImplemented("Http2ServerResponse.addTrailers"); + const state = res[kState]; + + if (this.headRequest !== state.headRequest) { + return; } - get connection(): Socket /*| TlsSocket*/ { - notImplemented("Http2ServerResponse.connection"); - return {}; + state.closed = true; + + this[kProxySocket] = null; + + this.removeListener("wantTrailers", onStreamTrailersReady); + this[kResponse] = undefined; + + res.emit("finish"); + res.emit("close"); +} + +function onStreamAbortedResponse() { + // non-op for now +} + +let statusMessageWarned = false; +let statusConnectionHeaderWarned = false; + +// Defines and implements an API compatibility layer on top of the core +// HTTP/2 implementation, intended to provide an interface that is as +// close as possible to the current require('http') API + +function statusMessageWarn() { + if (statusMessageWarned === false) { + emitWarning( + "Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)", + "UnsupportedWarning", + ); + statusMessageWarned = true; } +} - createPushResponse( - _headers: Record, - _callback: () => unknown, - ) { - notImplemented("Http2ServerResponse.createPushResponse"); +function isConnectionHeaderAllowed(name, value) { + return name !== constants.HTTP2_HEADER_CONNECTION || + value === "trailers"; +} + +function connectionHeaderMessageWarn() { + if (statusConnectionHeaderWarned === false) { + emitWarning( + "The provided connection header is not valid, " + + "the value will be dropped from the header and " + + "will never be in use.", + "UnsupportedWarning", + ); + statusConnectionHeaderWarned = true; } +} - end( - _data: string | Buffer | Uint8Array, - _encoding: string, - _callback: () => unknown, - ) { - notImplemented("Http2ServerResponse.end"); +class Http2ServerResponse extends Stream { + writable = false; + req = null; + + constructor(stream, options) { + super(options); + this[kState] = { + closed: false, + ending: false, + destroyed: false, + headRequest: false, + sendDate: true, + statusCode: constants.HTTP_STATUS_OK, + }; + this[kHeaders] = { __proto__: null }; + this[kTrailers] = { __proto__: null }; + this[kStream] = stream; + stream[kProxySocket] = null; + stream[kResponse] = this; + this.writable = true; + this.req = stream[kRequest]; + stream.on("drain", onStreamDrain); + stream.on("aborted", onStreamAbortedResponse); + stream.on("close", onStreamCloseResponse); + stream.on("wantTrailers", onStreamTrailersReady); + stream.on("timeout", onStreamTimeout(kResponse)); } - get finished(): boolean { - notImplemented("Http2ServerResponse.finished"); - return false; + // User land modules such as finalhandler just check truthiness of this + // but if someone is actually trying to use this for more than that + // then we simply can't support such use cases + get _header() { + return this.headersSent; } - getHeader(_name: string): string { - notImplemented("Http2ServerResponse.getHeader"); - return ""; + get writableEnded() { + const state = this[kState]; + return state.ending; } - getHeaderNames(): string[] { - notImplemented("Http2ServerResponse.getHeaderNames"); - return []; + get finished() { + const state = this[kState]; + return state.ending; } - getHeaders(): Record { - notImplemented("Http2ServerResponse.getHeaders"); - return {}; + get socket() { + // This is compatible with http1 which removes socket reference + // only from ServerResponse but not IncomingMessage + if (this[kState].closed) { + return undefined; + } + + const stream = this[kStream]; + const proxySocket = stream[kProxySocket]; + if (proxySocket === null) { + return stream[kProxySocket] = new Proxy(stream, proxySocketHandler); + } + return proxySocket; } - hasHeader(_name: string) { - notImplemented("Http2ServerResponse.hasHeader"); + get connection() { + return this.socket; } - get headersSent(): boolean { - notImplemented("Http2ServerResponse.headersSent"); - return false; + get stream() { + return this[kStream]; } - removeHeader(_name: string) { - notImplemented("Http2ServerResponse.removeHeader"); + get headersSent() { + return this[kStream].headersSent; } - get req(): Http2ServerRequest { - notImplemented("Http2ServerResponse.req"); - return new Http2ServerRequest(); + get sendDate() { + return this[kState].sendDate; } - get sendDate(): boolean { - notImplemented("Http2ServerResponse.sendDate"); - return false; + set sendDate(bool) { + this[kState].sendDate = Boolean(bool); } - setHeader(_name: string, _value: string | string[]) { - notImplemented("Http2ServerResponse.setHeader"); + get statusCode() { + return this[kState].statusCode; } - setTimeout(msecs: number, callback?: () => unknown) { - this.stream.setTimeout(msecs, callback); + get writableCorked() { + return this[kStream].writableCorked; } - get socket(): Socket /*| TlsSocket*/ { - notImplemented("Http2ServerResponse.socket"); - return {}; + get writableHighWaterMark() { + return this[kStream].writableHighWaterMark; } - get statusCode(): number { - notImplemented("Http2ServerResponse.statusCode"); - return 0; + get writableFinished() { + return this[kStream].writableFinished; + } + + get writableLength() { + return this[kStream].writableLength; + } + + set statusCode(code) { + code |= 0; + if (code >= 100 && code < 200) { + throw new ERR_HTTP2_INFO_STATUS_NOT_ALLOWED(); + } + if (code < 100 || code > 599) { + throw new ERR_HTTP2_STATUS_INVALID(code); + } + this[kState].statusCode = code; + } + + setTrailer(name, value) { + // validateString(name, "name"); + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + // assertValidHeader(name, value); + this[kTrailers][name] = value; } - get statusMessage(): string { - notImplemented("Http2ServerResponse.statusMessage"); + addTrailers(headers) { + const keys = ObjectKeys(headers); + let key = ""; + for (let i = 0; i < keys.length; i++) { + key = keys[i]; + this.setTrailer(key, headers[key]); + } + } + + getHeader(name) { + // validateString(name, "name"); + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + return this[kHeaders][name]; + } + + getHeaderNames() { + return ObjectKeys(this[kHeaders]); + } + + getHeaders() { + const headers = { __proto__: null }; + return ObjectAssign(headers, this[kHeaders]); + } + + hasHeader(name) { + // validateString(name, "name"); + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + return ObjectPrototypeHasOwnProperty(this[kHeaders], name); + } + + removeHeader(name) { + // validateString(name, "name"); + if (this[kStream].headersSent) { + throw new ERR_HTTP2_HEADERS_SENT(); + } + + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + + if (name === "date") { + this[kState].sendDate = false; + + return; + } + + delete this[kHeaders][name]; + } + + setHeader(name, value) { + // validateString(name, "name"); + if (this[kStream].headersSent) { + throw new ERR_HTTP2_HEADERS_SENT(); + } + + this[kSetHeader](name, value); + } + + [kSetHeader](name, value) { + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + // assertValidHeader(name, value); + + if (!isConnectionHeaderAllowed(name, value)) { + return; + } + + if (name[0] === ":") { + assertValidPseudoHeader(name); + } else if (!_checkIsHttpToken(name)) { + this.destroy(new ERR_INVALID_HTTP_TOKEN("Header name", name)); + } + + this[kHeaders][name] = value; + } + + appendHeader(name, value) { + // validateString(name, "name"); + if (this[kStream].headersSent) { + throw new ERR_HTTP2_HEADERS_SENT(); + } + + this[kAppendHeader](name, value); + } + + [kAppendHeader](name, value) { + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + // assertValidHeader(name, value); + + if (!isConnectionHeaderAllowed(name, value)) { + return; + } + + if (name[0] === ":") { + assertValidPseudoHeader(name); + } else if (!_checkIsHttpToken(name)) { + this.destroy(new ERR_INVALID_HTTP_TOKEN("Header name", name)); + } + + // Handle various possible cases the same as OutgoingMessage.appendHeader: + const headers = this[kHeaders]; + if (headers === null || !headers[name]) { + return this.setHeader(name, value); + } + + if (!ArrayIsArray(headers[name])) { + headers[name] = [headers[name]]; + } + + const existingValues = headers[name]; + if (ArrayIsArray(value)) { + for (let i = 0, length = value.length; i < length; i++) { + existingValues.push(value[i]); + } + } else { + existingValues.push(value); + } + } + + get statusMessage() { + statusMessageWarn(); + return ""; } - get stream(): Http2Stream { - notImplemented("Http2ServerResponse.stream"); - return new Http2Stream(); + set statusMessage(msg) { + statusMessageWarn(); } - get writableEnded(): boolean { - notImplemented("Http2ServerResponse.writableEnded"); - return false; + flushHeaders() { + const state = this[kState]; + if (!state.closed && !this[kStream].headersSent) { + this.writeHead(state.statusCode); + } } - write( - _chunk: string | Buffer | Uint8Array, - _encoding: string, - _callback: () => unknown, - ) { - notImplemented("Http2ServerResponse.write"); - return this.write; + writeHead(statusCode, statusMessage, headers) { + const state = this[kState]; + + if (state.closed || this.stream.destroyed) { + return this; + } + if (this[kStream].headersSent) { + throw new ERR_HTTP2_HEADERS_SENT(); + } + + if (typeof statusMessage === "string") { + statusMessageWarn(); + } + + if (headers === undefined && typeof statusMessage === "object") { + headers = statusMessage; + } + + let i; + if (ArrayIsArray(headers)) { + if (this[kHeaders]) { + // Headers in obj should override previous headers but still + // allow explicit duplicates. To do so, we first remove any + // existing conflicts, then use appendHeader. This is the + // slow path, which only applies when you use setHeader and + // then pass headers in writeHead too. + + // We need to handle both the tuple and flat array formats, just + // like the logic further below. + if (headers.length && ArrayIsArray(headers[0])) { + for (let n = 0; n < headers.length; n += 1) { + const key = headers[n + 0][0]; + this.removeHeader(key); + } + } else { + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0]; + this.removeHeader(key); + } + } + } + + // Append all the headers provided in the array: + if (headers.length && ArrayIsArray(headers[0])) { + for (i = 0; i < headers.length; i++) { + const header = headers[i]; + this[kAppendHeader](header[0], header[1]); + } + } else { + if (headers.length % 2 !== 0) { + throw new ERR_INVALID_ARG_VALUE("headers", headers); + } + + for (i = 0; i < headers.length; i += 2) { + this[kAppendHeader](headers[i], headers[i + 1]); + } + } + } else if (typeof headers === "object") { + const keys = ObjectKeys(headers); + let key = ""; + for (i = 0; i < keys.length; i++) { + key = keys[i]; + this[kSetHeader](key, headers[key]); + } + } + + state.statusCode = statusCode; + this[kBeginSend](); + + return this; } - writeContinue() { - notImplemented("Http2ServerResponse.writeContinue"); + cork() { + this[kStream].cork(); } - writeEarlyHints(_hints: Record) { - notImplemented("Http2ServerResponse.writeEarlyHints"); + uncork() { + this[kStream].uncork(); } - writeHead( - _statusCode: number, - _statusMessage: string, - _headers: Record, - ) { - notImplemented("Http2ServerResponse.writeHead"); + write(chunk, encoding, cb) { + const state = this[kState]; + + if (typeof encoding === "function") { + cb = encoding; + encoding = "utf8"; + } + + let err; + if (state.ending) { + err = new ERR_STREAM_WRITE_AFTER_END(); + } else if (state.closed) { + err = new ERR_HTTP2_INVALID_STREAM(); + } else if (state.destroyed) { + return false; + } + + if (err) { + if (typeof cb === "function") { + nextTick(cb, err); + } + this.destroy(err); + return false; + } + + const stream = this[kStream]; + if (!stream.headersSent) { + this.writeHead(state.statusCode); + } + return stream.write(chunk, encoding, cb); + } + + end(chunk, encoding, cb) { + const stream = this[kStream]; + const state = this[kState]; + + if (typeof chunk === "function") { + cb = chunk; + chunk = null; + } else if (typeof encoding === "function") { + cb = encoding; + encoding = "utf8"; + } + + if ( + (state.closed || state.ending) && + state.headRequest === stream.headRequest + ) { + if (typeof cb === "function") { + nextTick(cb); + } + return this; + } + + if (chunk !== null && chunk !== undefined) { + this.write(chunk, encoding); + } + + state.headRequest = stream.headRequest; + state.ending = true; + + if (typeof cb === "function") { + if (stream.writableEnded) { + this.once("finish", cb); + } else { + stream.once("finish", cb); + } + } + + if (!stream.headersSent) { + this.writeHead(this[kState].statusCode); + } + + if (this[kState].closed || stream.destroyed) { + ReflectApply(onStreamCloseResponse, stream, []); + } else { + stream.end(); + } + + return this; + } + + destroy(err) { + if (this[kState].destroyed) { + return; + } + + this[kState].destroyed = true; + this[kStream].destroy(err); + } + + setTimeout(msecs, callback) { + if (this[kState].closed) { + return; + } + this[kStream].setTimeout(msecs, callback); + } + + createPushResponse(headers, callback) { + // validateFunction(callback, "callback"); + if (this[kState].closed) { + nextTick(callback, new ERR_HTTP2_INVALID_STREAM()); + return; + } + this[kStream].pushStream(headers, {}, (err, stream, headers, options) => { + if (err) { + callback(err); + return; + } + callback(null, new Http2ServerResponse(stream)); + }); + } + + [kBeginSend]() { + const state = this[kState]; + const headers = this[kHeaders]; + headers[constants.HTTP2_HEADER_STATUS] = state.statusCode; + const options = { + endStream: state.ending, + waitForTrailers: true, + sendDate: state.sendDate, + }; + this[kStream].respond(headers, options); + } + + // TODO doesn't support callbacks + writeContinue() { + const stream = this[kStream]; + if (stream.headersSent || this[kState].closed) { + return false; + } + stream.additionalHeaders({ + [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_CONTINUE, + }); + return true; + } + + writeEarlyHints(hints) { + // validateObject(hints, "hints"); + + const headers = { __proto__: null }; + + // const linkHeaderValue = validateLinkHeaderValue(hints.link); + + for (const key of ObjectKeys(hints)) { + if (key !== "link") { + headers[key] = hints[key]; + } + } + + // if (linkHeaderValue.length === 0) { + // return false; + // } + + const stream = this[kStream]; + + if (stream.headersSent || this[kState].closed) { + return false; + } + + stream.additionalHeaders({ + ...headers, + [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_EARLY_HINTS, + // "Link": linkHeaderValue, + }); + + return true; } } +// export class Http2ServerResponse { +// constructor() { +// } + +// addTrailers(_headers: Record) { +// notImplemented("Http2ServerResponse.addTrailers"); +// } + +// get connection(): Socket /*| TlsSocket*/ { +// notImplemented("Http2ServerResponse.connection"); +// return {}; +// } + +// createPushResponse( +// _headers: Record, +// _callback: () => unknown, +// ) { +// notImplemented("Http2ServerResponse.createPushResponse"); +// } + +// end( +// _data: string | Buffer | Uint8Array, +// _encoding: string, +// _callback: () => unknown, +// ) { +// notImplemented("Http2ServerResponse.end"); +// } + +// get finished(): boolean { +// notImplemented("Http2ServerResponse.finished"); +// return false; +// } + +// getHeader(_name: string): string { +// notImplemented("Http2ServerResponse.getHeader"); +// return ""; +// } + +// getHeaderNames(): string[] { +// notImplemented("Http2ServerResponse.getHeaderNames"); +// return []; +// } + +// getHeaders(): Record { +// notImplemented("Http2ServerResponse.getHeaders"); +// return {}; +// } + +// hasHeader(_name: string) { +// notImplemented("Http2ServerResponse.hasHeader"); +// } + +// get headersSent(): boolean { +// notImplemented("Http2ServerResponse.headersSent"); +// return false; +// } + +// removeHeader(_name: string) { +// notImplemented("Http2ServerResponse.removeHeader"); +// } + +// get req(): Http2ServerRequest { +// notImplemented("Http2ServerResponse.req"); +// return new Http2ServerRequest(); +// } + +// get sendDate(): boolean { +// notImplemented("Http2ServerResponse.sendDate"); +// return false; +// } + +// setHeader(_name: string, _value: string | string[]) { +// notImplemented("Http2ServerResponse.setHeader"); +// } + +// setTimeout(msecs: number, callback?: () => unknown) { +// this.stream.setTimeout(msecs, callback); +// } + +// get socket(): Socket /*| TlsSocket*/ { +// notImplemented("Http2ServerResponse.socket"); +// return {}; +// } + +// get statusCode(): number { +// notImplemented("Http2ServerResponse.statusCode"); +// return 0; +// } + +// get statusMessage(): string { +// notImplemented("Http2ServerResponse.statusMessage"); +// return ""; +// } + +// get stream(): Http2Stream { +// notImplemented("Http2ServerResponse.stream"); +// return new Http2Stream(); +// } + +// get writableEnded(): boolean { +// notImplemented("Http2ServerResponse.writableEnded"); +// return false; +// } + +// write( +// _chunk: string | Buffer | Uint8Array, +// _encoding: string, +// _callback: () => unknown, +// ) { +// notImplemented("Http2ServerResponse.write"); +// return this.write; +// } + +// writeContinue() { +// notImplemented("Http2ServerResponse.writeContinue"); +// } + +// writeEarlyHints(_hints: Record) { +// notImplemented("Http2ServerResponse.writeEarlyHints"); +// } + +// writeHead( +// _statusCode: number, +// _statusMessage: string, +// _headers: Record, +// ) { +// notImplemented("Http2ServerResponse.writeHead"); +// } +// } + export default { createServer, createSecureServer, diff --git a/main.mjs b/main.mjs new file mode 100644 index 00000000000000..3e3f49bce7cabc --- /dev/null +++ b/main.mjs @@ -0,0 +1,7 @@ +const { addTrailers } = Deno[Deno.internal]; + +Deno.serve(async (req) => { + const res = new Response("Hello World"); + addTrailers(res, { "X-Foo": "bar" }); + return res; +}); diff --git a/tests/unit/serve_test.ts b/tests/unit/serve_test.ts index 5d83aa5fc66590..635f164e49dfc2 100644 --- a/tests/unit/serve_test.ts +++ b/tests/unit/serve_test.ts @@ -3708,7 +3708,7 @@ Deno.test( // TODO(mmastrac): This test should eventually use fetch, when we support trailers there. // This test is ignored because it's flaky and relies on cURL's verbose output. Deno.test( - { permissions: { net: true, run: true, read: true }, ignore: true }, + { permissions: { net: true, run: true, read: true }, ignore: false }, async function httpServerTrailers() { const ac = new AbortController(); const { resolve } = Promise.withResolvers(); From cdaa729a08b404e57faa8bb55bba3eb715be1913 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Tue, 5 Mar 2024 17:56:31 +0530 Subject: [PATCH 03/15] refactor: move test_util.ts --- tests/unit/chmod_test.ts | 2 +- tests/unit/command_test.ts | 2 +- tests/unit/read_file_test.ts | 2 +- tests/unit/serve_test.ts | 2 +- tests/unit/symlink_test.ts | 2 +- tests/unit/tls_test.ts | 2 +- tests/unit/write_file_test.ts | 2 +- tests/{unit => util}/test_util.ts | 0 8 files changed, 7 insertions(+), 7 deletions(-) rename tests/{unit => util}/test_util.ts (100%) diff --git a/tests/unit/chmod_test.ts b/tests/unit/chmod_test.ts index df3771bbc1631d..5e22d6b60a7eaf 100644 --- a/tests/unit/chmod_test.ts +++ b/tests/unit/chmod_test.ts @@ -4,7 +4,7 @@ import { assertEquals, assertRejects, assertThrows, -} from "./test_util.ts"; +} from "../util/test_util.ts"; Deno.test( { diff --git a/tests/unit/command_test.ts b/tests/unit/command_test.ts index cbb1c4921c9278..ac540ef26cb97c 100644 --- a/tests/unit/command_test.ts +++ b/tests/unit/command_test.ts @@ -6,7 +6,7 @@ import { assertRejects, assertStringIncludes, assertThrows, -} from "./test_util.ts"; +} from "../util/test_util.ts"; Deno.test( { permissions: { write: true, run: true, read: true } }, diff --git a/tests/unit/read_file_test.ts b/tests/unit/read_file_test.ts index bfb3b508512887..8b34cc58f09645 100644 --- a/tests/unit/read_file_test.ts +++ b/tests/unit/read_file_test.ts @@ -6,7 +6,7 @@ import { assertThrows, pathToAbsoluteFileUrl, unreachable, -} from "./test_util.ts"; +} from "../util/test_util.ts"; Deno.test({ permissions: { read: true } }, function readFileSyncSuccess() { const data = Deno.readFileSync("tests/testdata/assets/fixture.json"); diff --git a/tests/unit/serve_test.ts b/tests/unit/serve_test.ts index 635f164e49dfc2..f1d9be7c5766b5 100644 --- a/tests/unit/serve_test.ts +++ b/tests/unit/serve_test.ts @@ -11,7 +11,7 @@ import { execCode, fail, tmpUnixSocketPath, -} from "./test_util.ts"; +} from "../util/test_util.ts"; // Since these tests may run in parallel, ensure this port is unique to this file const servePort = 4502; diff --git a/tests/unit/symlink_test.ts b/tests/unit/symlink_test.ts index 310c3693055d73..f8498747603ee0 100644 --- a/tests/unit/symlink_test.ts +++ b/tests/unit/symlink_test.ts @@ -4,7 +4,7 @@ import { assertRejects, assertThrows, pathToAbsoluteFileUrl, -} from "./test_util.ts"; +} from "../util/test_util.ts"; Deno.test( { permissions: { read: true, write: true } }, diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 1ac5e8d9832c59..ea4e8c43f27eb6 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -6,7 +6,7 @@ import { assertRejects, assertStrictEquals, assertThrows, -} from "./test_util.ts"; +} from "../util/test_util.ts"; import { BufReader, BufWriter } from "@std/io/mod.ts"; import { readAll } from "@std/streams/read_all.ts"; import { writeAll } from "@std/streams/write_all.ts"; diff --git a/tests/unit/write_file_test.ts b/tests/unit/write_file_test.ts index 6cd08e2d14db98..1ba526ca62a1b1 100644 --- a/tests/unit/write_file_test.ts +++ b/tests/unit/write_file_test.ts @@ -5,7 +5,7 @@ import { assertRejects, assertThrows, unreachable, -} from "./test_util.ts"; +} from "../util/test_util.ts"; Deno.test( { permissions: { read: true, write: true } }, diff --git a/tests/unit/test_util.ts b/tests/util/test_util.ts similarity index 100% rename from tests/unit/test_util.ts rename to tests/util/test_util.ts From 9963629906365fe113fc173d5e98ec2df27338ec Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Tue, 5 Mar 2024 18:03:55 +0530 Subject: [PATCH 04/15] refactor: move curlRequest and curlRequestWithStdErr --- tests/unit/serve_test.ts | 28 ++-------------------------- tests/util/test_util.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/tests/unit/serve_test.ts b/tests/unit/serve_test.ts index f1d9be7c5766b5..02d549732f816c 100644 --- a/tests/unit/serve_test.ts +++ b/tests/unit/serve_test.ts @@ -8,6 +8,8 @@ import { assertEquals, assertStringIncludes, assertThrows, + curlRequest, + curlRequestWithStdErr, execCode, fail, tmpUnixSocketPath, @@ -3793,32 +3795,6 @@ Deno.test( }, ); -async function curlRequest(args: string[]) { - const { success, stdout, stderr } = await new Deno.Command("curl", { - args, - stdout: "piped", - stderr: "piped", - }).output(); - assert( - success, - `Failed to cURL ${args}: stdout\n\n${stdout}\n\nstderr:\n\n${stderr}`, - ); - return new TextDecoder().decode(stdout); -} - -async function curlRequestWithStdErr(args: string[]) { - const { success, stdout, stderr } = await new Deno.Command("curl", { - args, - stdout: "piped", - stderr: "piped", - }).output(); - assert( - success, - `Failed to cURL ${args}: stdout\n\n${stdout}\n\nstderr:\n\n${stderr}`, - ); - return [new TextDecoder().decode(stdout), new TextDecoder().decode(stderr)]; -} - Deno.test("Deno.HttpServer is not thenable", async () => { // deno-lint-ignore require-await async function serveTest() { diff --git a/tests/util/test_util.ts b/tests/util/test_util.ts index c73f52b1589eaa..77424b46993d12 100644 --- a/tests/util/test_util.ts +++ b/tests/util/test_util.ts @@ -85,3 +85,35 @@ export function tmpUnixSocketPath(): string { const folder = Deno.makeTempDirSync(); return join(folder, "socket"); } + +export async function curlRequest(args: string[]) { + const { success, stdout, stderr } = await new Deno.Command("curl", { + args, + stdout: "piped", + stderr: "piped", + }).output(); + const decoder = new TextDecoder(); + assert( + success, + `Failed to cURL ${args}: stdout\n\n${ + decoder.decode(stdout) + }\n\nstderr:\n\n${decoder.decode(stderr)}`, + ); + return decoder.decode(stdout); +} + +export async function curlRequestWithStdErr(args: string[]) { + const { success, stdout, stderr } = await new Deno.Command("curl", { + args, + stdout: "piped", + stderr: "piped", + }).output(); + let decoder = new TextDecoder(); + assert( + success, + `Failed to cURL ${args}: stdout\n\n${ + decoder.decode(stdout) + }\n\nstderr:\n\n${decoder.decode(stderr)}`, + ); + return [decoder.decode(stdout), decoder.decode(stderr)]; +} From 6f0b85555d62417b384d9d137047c45576ac747e Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Tue, 5 Mar 2024 18:26:23 +0530 Subject: [PATCH 05/15] fix: update imports of test_util.ts --- tests/unit/files_test.ts | 2 +- tests/unit/real_path_test.ts | 2 +- tests/unit_node/dgram_test.ts | 2 +- tests/unit_node/fs_test.ts | 2 +- tests/unit_node/http2_test.ts | 21 +++++++++++++++++++++ tests/unit_node/http_test.ts | 2 +- 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/tests/unit/files_test.ts b/tests/unit/files_test.ts index 0034a847222017..57a2bb305c1fc6 100644 --- a/tests/unit/files_test.ts +++ b/tests/unit/files_test.ts @@ -7,7 +7,7 @@ import { assertEquals, assertRejects, assertThrows, -} from "./test_util.ts"; +} from "../util/test_util.ts"; import { copy } from "@std/streams/copy.ts"; // Note tests for Deno.FsFile.setRaw is in integration tests. diff --git a/tests/unit/real_path_test.ts b/tests/unit/real_path_test.ts index b3656a927cd5db..30a9b62016e112 100644 --- a/tests/unit/real_path_test.ts +++ b/tests/unit/real_path_test.ts @@ -6,7 +6,7 @@ import { assertRejects, assertThrows, pathToAbsoluteFileUrl, -} from "./test_util.ts"; +} from "../util/test_util.ts"; Deno.test({ permissions: { read: true } }, function realPathSyncSuccess() { const relative = "tests/testdata/assets/fixture.json"; diff --git a/tests/unit_node/dgram_test.ts b/tests/unit_node/dgram_test.ts index 10c8e52537dec5..425649d5222205 100644 --- a/tests/unit_node/dgram_test.ts +++ b/tests/unit_node/dgram_test.ts @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertEquals } from "@std/assert/mod.ts"; -import { execCode } from "../unit/test_util.ts"; +import { execCode } from "../util/test_util.ts"; import { createSocket } from "node:dgram"; const listenPort = 4503; diff --git a/tests/unit_node/fs_test.ts b/tests/unit_node/fs_test.ts index 4a8ef89f3da0f7..3454e95d4a8e2d 100644 --- a/tests/unit_node/fs_test.ts +++ b/tests/unit_node/fs_test.ts @@ -12,7 +12,7 @@ import { writeFileSync, } from "node:fs"; import { constants as fsPromiseConstants, cp } from "node:fs/promises"; -import { pathToAbsoluteFileUrl } from "../unit/test_util.ts"; +import { pathToAbsoluteFileUrl } from "../util/test_util.ts"; Deno.test( "[node/fs writeFileSync] write file without option", diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index fd9cdd0ec8a091..2666e13f52640f 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -3,6 +3,7 @@ import * as http2 from "node:http2"; import * as net from "node:net"; import { assert, assertEquals } from "@std/assert/mod.ts"; +import { curlRequest } from "../util/test_util"; for (const url of ["http://127.0.0.1:4246", "https://127.0.0.1:4247"]) { Deno.test(`[node/http2 client] ${url}`, { @@ -165,3 +166,23 @@ Deno.test("[node/http2 client GET https://www.example.com]", async () => { assertEquals(status, 200); assert(chunk.length > 0); }); + +Deno.test("[node/http2.createServer()]", { sanitizeOps: false }, async () => { + const server = http2.createServer((req, res) => { + res.setHeader("Content-Type", "text/html"); + res.setHeader("X-Foo", "bar"); + res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); + res.write("Hello, World!"); + res.end(); + }); + server.listen(0); + const port = ( server.address()).port; + const endpoint = `http://localhost:${port}`; + + const response = await curlRequest([ + endpoint, + "--http2-prior-knowledge", + ]); + assertEquals(response, "Hello, World!"); + server.close(); +}); diff --git a/tests/unit_node/http_test.ts b/tests/unit_node/http_test.ts index 57ade6298a589a..db0635e136530e 100644 --- a/tests/unit_node/http_test.ts +++ b/tests/unit_node/http_test.ts @@ -10,7 +10,7 @@ import { assertSpyCalls, spy } from "@std/testing/mock.ts"; import { gzip } from "node:zlib"; import { Buffer } from "node:buffer"; import { serve } from "@std/http/server.ts"; -import { execCode } from "../unit/test_util.ts"; +import { execCode } from "../util/test_util.ts"; Deno.test("[node/http listen]", async () => { { From 749b6e45e58ac87188027f79fe403d130f86bd73 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Tue, 5 Mar 2024 19:57:14 +0530 Subject: [PATCH 06/15] http2.createServer() works --- a.mjs | 14 -- ext/node/polyfills/http2.ts | 278 ++-------------------------------- ext/node/polyfills/net.ts | 7 +- main.mjs | 7 - tests/unit_node/http2_test.ts | 4 +- tests/util/test_util.ts | 1 + 6 files changed, 22 insertions(+), 289 deletions(-) delete mode 100644 a.mjs delete mode 100644 main.mjs diff --git a/a.mjs b/a.mjs deleted file mode 100644 index 53adf114b9ee70..00000000000000 --- a/a.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import http2 from "node:http2"; - -const server = http2.createServer((req, res) => { - console.log("handler called"); - console.log(req); - res.setHeader("Content-Type", "text/html"); - res.setHeader("X-Foo", "bar"); - res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); - res.write("Hello, World!"); - console.log(res); - res.end(); -}); - -server.listen(8000); diff --git a/ext/node/polyfills/http2.ts b/ext/node/polyfills/http2.ts index 3ac1c152a67163..3edbcfc5541c13 100644 --- a/ext/node/polyfills/http2.ts +++ b/ext/node/polyfills/http2.ts @@ -15,9 +15,11 @@ import { op_http2_client_send_trailers, op_http2_connect, op_http2_poll_client_connection, + op_http_set_response_trailers, } from "ext:core/ops"; import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; +import { toInnerRequest } from "ext:deno_fetch/23_request.js"; import { Readable } from "node:stream"; import { EventEmitter } from "node:events"; import { Buffer } from "node:buffer"; @@ -34,7 +36,7 @@ import { } from "ext:deno_node/internal/stream_base_commons.ts"; import { FileHandle } from "node:fs/promises"; import { kStreamBaseField } from "ext:deno_node/internal_binding/stream_wrap.ts"; -import { addTrailers, serveHttpOnConnection } from "ext:deno_http/00_serve.js"; +import { serveHttpOnConnection } from "ext:deno_http/00_serve.js"; import { nextTick } from "ext:deno_node/_next_tick.ts"; import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; import { Duplex } from "node:stream"; @@ -117,7 +119,7 @@ const SESSION_FLAGS_DESTROYED = 0x4; const ENCODER = new TextEncoder(); type Http2Headers = Record; -const debugHttp2Enabled = true; +const debugHttp2Enabled = false; function debugHttp2(...args) { if (debugHttp2Enabled) { console.log(...args); @@ -355,9 +357,6 @@ function closeSession(session: Http2Session, code?: number, error?: Error) { export class ServerHttp2Session extends Http2Session { constructor() { super(constants.NGHTTP2_SESSION_SERVER, {}); - this.on("stream", (stream, headers) => { - console.log(stream, headers); - }); } altsvc( @@ -591,6 +590,8 @@ export class Http2Stream extends EventEmitter { #readerPromise: Promise>; #closed: boolean; _response: Response; + // This is required to set the trailers on the response. + _request: Request; constructor( session: Http2Session, @@ -725,10 +726,8 @@ export class Http2Stream extends EventEmitter { } sendTrailers(headers: Record) { - console.log("sendTrailers(headers): ", headers); - - // addTrailers(this._response, [["grpc-status", "0"], ["grpc-message", "OK"]]); - addTrailers(this._response, Object.entries(headers)); + let request = toInnerRequest(this._request); + op_http_set_response_trailers(request.external, Object.entries(headers)); } } @@ -1300,10 +1299,13 @@ export class ServerHttp2Stream extends Http2Stream { controllerPromise: Promise>, reader: ReadableStream, body: ReadableStream, + // This is required to set the trailers on the response. + req: Request, ) { super(session, headers, controllerPromise, Promise.resolve(reader)); this._deferred = Promise.withResolvers(); this.#body = body; + this._request = req; } additionalHeaders(_headers: Record) { @@ -1338,12 +1340,6 @@ export class ServerHttp2Stream extends Http2Stream { headers: Http2Headers, options: Record, ) { - debugHttp2( - "ServerHttp2Stream.respond() headers:", - headers, - "options:", - options, - ); this.#headersSent = true; const response: ResponseInit = {}; if (headers) { @@ -1405,7 +1401,6 @@ function onServerStream( flags, rawHeaders, ) { - console.log("handling on stream event"); const server = this; const request = new ServerRequest(stream, headers, undefined, rawHeaders); const response = new ServerResponse(stream); @@ -1504,7 +1499,6 @@ export class Http2Server extends Server { this.on( "connection", (conn: Deno.Conn) => { - console.log("got connection: ", conn); try { const session = new ServerHttp2Session(); this.emit("session", session); @@ -1512,9 +1506,7 @@ export class Http2Server extends Server { conn, this.#abortController.signal, async (req: Request) => { - console.log("got request", req); try { - console.log("inside try block"); const controllerDeferred = Promise.withResolvers< ReadableStreamDefaultController >(); @@ -1523,28 +1515,22 @@ export class Http2Server extends Server { controllerDeferred.resolve(controller); }, }); - console.log("before headers"); const headers: Http2Headers = {}; for (const [name, value] of req.headers) { headers[name] = value; } headers[constants.HTTP2_HEADER_PATH] = new URL(req.url).pathname; - console.log("headers: ", headers); const stream = new ServerHttp2Stream( session, Promise.resolve(headers), controllerDeferred.promise, req.body, body, + req, ); - console.log("stream created successfully"); - session.emit("stream", stream, headers); this.emit("stream", stream, headers); - console.log("stream emitted"); - let res = await stream._deferred.promise; - console.log("res: ", res); - return res; + return await stream._deferred.promise; } catch (e) { console.log(">>> Error in serveHttpOnConnection", e); } @@ -1571,14 +1557,6 @@ export class Http2Server extends Server { return clientHandle[kStreamBaseField]; } - close(callback?: () => unknown) { - if (callback) { - this.on("close", callback); - } - this.#abortController.abort(); - super.close(); - } - setTimeout(msecs: number, callback?: () => unknown) { this.timeout = msecs; if (callback !== undefined) { @@ -2066,6 +2044,7 @@ const kSocket = Symbol("socket"); const kTrailers = Symbol("trailers"); const kRawTrailers = Symbol("rawTrailers"); const kSetHeader = Symbol("setHeader"); +const kServer = Symbol("server"); const kAppendHeader = Symbol("appendHeader"); const kAborted = Symbol("aborted"); const kProxySocket = Symbol("proxySocket"); @@ -2366,89 +2345,6 @@ function onStreamAbortedRequest() { } } -// export class Http2ServerRequest extends Readable { -// constructor() { -// } - -// get aborted(): boolean { -// notImplemented("Http2ServerRequest.aborted"); -// return false; -// } - -// get authority(): string { -// notImplemented("Http2ServerRequest.authority"); -// return ""; -// } - -// get complete(): boolean { -// notImplemented("Http2ServerRequest.complete"); -// return false; -// } - -// get connection(): Socket /*| TlsSocket*/ { -// notImplemented("Http2ServerRequest.connection"); -// return {}; -// } - -// destroy(_error: Error) { -// notImplemented("Http2ServerRequest.destroy"); -// } - -// get headers(): Record { -// notImplemented("Http2ServerRequest.headers"); -// return {}; -// } - -// get httpVersion(): string { -// notImplemented("Http2ServerRequest.httpVersion"); -// return ""; -// } - -// get method(): string { -// notImplemented("Http2ServerRequest.method"); -// return ""; -// } - -// get rawHeaders(): string[] { -// notImplemented("Http2ServerRequest.rawHeaders"); -// return []; -// } - -// get rawTrailers(): string[] { -// notImplemented("Http2ServerRequest.rawTrailers"); -// return []; -// } - -// get scheme(): string { -// notImplemented("Http2ServerRequest.scheme"); -// return ""; -// } - -// setTimeout(msecs: number, callback?: () => unknown) { -// this.stream.setTimeout(callback, msecs); -// } - -// get socket(): Socket /*| TlsSocket*/ { -// notImplemented("Http2ServerRequest.socket"); -// return {}; -// } - -// get stream(): Http2Stream { -// notImplemented("Http2ServerRequest.stream"); -// return new Http2Stream(); -// } - -// get trailers(): Record { -// notImplemented("Http2ServerRequest.trailers"); -// return {}; -// } - -// get url(): string { -// notImplemented("Http2ServerRequest.url"); -// return ""; -// } -// } - function onStreamTrailersReady() { this.sendTrailers(this[kResponse][kTrailers]); } @@ -2503,18 +2399,6 @@ function isConnectionHeaderAllowed(name, value) { value === "trailers"; } -function connectionHeaderMessageWarn() { - if (statusConnectionHeaderWarned === false) { - emitWarning( - "The provided connection header is not valid, " + - "the value will be dropped from the header and " + - "will never be in use.", - "UnsupportedWarning", - ); - statusConnectionHeaderWarned = true; - } -} - class Http2ServerResponse extends Stream { writable = false; req = null; @@ -2950,12 +2834,12 @@ class Http2ServerResponse extends Stream { nextTick(callback, new ERR_HTTP2_INVALID_STREAM()); return; } - this[kStream].pushStream(headers, {}, (err, stream, headers, options) => { + this[kStream].pushStream(headers, {}, (err, stream, _headers, options) => { if (err) { callback(err); return; } - callback(null, new Http2ServerResponse(stream)); + callback(null, new Http2ServerResponse(stream, options)); }); } @@ -3016,136 +2900,6 @@ class Http2ServerResponse extends Stream { } } -// export class Http2ServerResponse { -// constructor() { -// } - -// addTrailers(_headers: Record) { -// notImplemented("Http2ServerResponse.addTrailers"); -// } - -// get connection(): Socket /*| TlsSocket*/ { -// notImplemented("Http2ServerResponse.connection"); -// return {}; -// } - -// createPushResponse( -// _headers: Record, -// _callback: () => unknown, -// ) { -// notImplemented("Http2ServerResponse.createPushResponse"); -// } - -// end( -// _data: string | Buffer | Uint8Array, -// _encoding: string, -// _callback: () => unknown, -// ) { -// notImplemented("Http2ServerResponse.end"); -// } - -// get finished(): boolean { -// notImplemented("Http2ServerResponse.finished"); -// return false; -// } - -// getHeader(_name: string): string { -// notImplemented("Http2ServerResponse.getHeader"); -// return ""; -// } - -// getHeaderNames(): string[] { -// notImplemented("Http2ServerResponse.getHeaderNames"); -// return []; -// } - -// getHeaders(): Record { -// notImplemented("Http2ServerResponse.getHeaders"); -// return {}; -// } - -// hasHeader(_name: string) { -// notImplemented("Http2ServerResponse.hasHeader"); -// } - -// get headersSent(): boolean { -// notImplemented("Http2ServerResponse.headersSent"); -// return false; -// } - -// removeHeader(_name: string) { -// notImplemented("Http2ServerResponse.removeHeader"); -// } - -// get req(): Http2ServerRequest { -// notImplemented("Http2ServerResponse.req"); -// return new Http2ServerRequest(); -// } - -// get sendDate(): boolean { -// notImplemented("Http2ServerResponse.sendDate"); -// return false; -// } - -// setHeader(_name: string, _value: string | string[]) { -// notImplemented("Http2ServerResponse.setHeader"); -// } - -// setTimeout(msecs: number, callback?: () => unknown) { -// this.stream.setTimeout(msecs, callback); -// } - -// get socket(): Socket /*| TlsSocket*/ { -// notImplemented("Http2ServerResponse.socket"); -// return {}; -// } - -// get statusCode(): number { -// notImplemented("Http2ServerResponse.statusCode"); -// return 0; -// } - -// get statusMessage(): string { -// notImplemented("Http2ServerResponse.statusMessage"); -// return ""; -// } - -// get stream(): Http2Stream { -// notImplemented("Http2ServerResponse.stream"); -// return new Http2Stream(); -// } - -// get writableEnded(): boolean { -// notImplemented("Http2ServerResponse.writableEnded"); -// return false; -// } - -// write( -// _chunk: string | Buffer | Uint8Array, -// _encoding: string, -// _callback: () => unknown, -// ) { -// notImplemented("Http2ServerResponse.write"); -// return this.write; -// } - -// writeContinue() { -// notImplemented("Http2ServerResponse.writeContinue"); -// } - -// writeEarlyHints(_hints: Record) { -// notImplemented("Http2ServerResponse.writeEarlyHints"); -// } - -// writeHead( -// _statusCode: number, -// _statusMessage: string, -// _headers: Record, -// ) { -// notImplemented("Http2ServerResponse.writeHead"); -// } -// } - export default { createServer, createSecureServer, diff --git a/ext/node/polyfills/net.ts b/ext/node/polyfills/net.ts index 6f81d69b823d82..66b7735d959728 100644 --- a/ext/node/polyfills/net.ts +++ b/ext/node/polyfills/net.ts @@ -106,10 +106,9 @@ import type { BufferEncoding } from "ext:deno_node/_global.d.ts"; import type { Abortable } from "ext:deno_node/_events.d.ts"; import { channel } from "node:diagnostics_channel"; -// let debug = debuglog("net", (fn) => { -// debug = fn; -// }); -let debug = console.log; +let debug = debuglog("net", (fn) => { + debug = fn; +}); const kLastWriteQueueSize = Symbol("lastWriteQueueSize"); const kSetNoDelay = Symbol("kSetNoDelay"); diff --git a/main.mjs b/main.mjs deleted file mode 100644 index 3e3f49bce7cabc..00000000000000 --- a/main.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const { addTrailers } = Deno[Deno.internal]; - -Deno.serve(async (req) => { - const res = new Response("Hello World"); - addTrailers(res, { "X-Foo": "bar" }); - return res; -}); diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index 2666e13f52640f..a37d0991f319f8 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -3,7 +3,7 @@ import * as http2 from "node:http2"; import * as net from "node:net"; import { assert, assertEquals } from "@std/assert/mod.ts"; -import { curlRequest } from "../util/test_util"; +import { curlRequest } from "../util/test_util.ts"; for (const url of ["http://127.0.0.1:4246", "https://127.0.0.1:4247"]) { Deno.test(`[node/http2 client] ${url}`, { @@ -168,7 +168,7 @@ Deno.test("[node/http2 client GET https://www.example.com]", async () => { }); Deno.test("[node/http2.createServer()]", { sanitizeOps: false }, async () => { - const server = http2.createServer((req, res) => { + const server = http2.createServer((_req, res) => { res.setHeader("Content-Type", "text/html"); res.setHeader("X-Foo", "bar"); res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); diff --git a/tests/util/test_util.ts b/tests/util/test_util.ts index 77424b46993d12..12ead9455c0b4d 100644 --- a/tests/util/test_util.ts +++ b/tests/util/test_util.ts @@ -1,6 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import * as colors from "@std/fmt/colors.ts"; +import { assert } from "@std/assert/mod.ts"; export { colors }; import { join, resolve } from "@std/path/mod.ts"; export { From 9970b430de561dea172a65abcfdc89a0d6cfb5a8 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Tue, 5 Mar 2024 20:28:43 +0530 Subject: [PATCH 07/15] fix lint --- ext/node/polyfills/http2.ts | 35 +++++++++++++++-------------------- tests/util/test_util.ts | 2 +- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/ext/node/polyfills/http2.ts b/ext/node/polyfills/http2.ts index 3edbcfc5541c13..8b5c16b2afcae6 100644 --- a/ext/node/polyfills/http2.ts +++ b/ext/node/polyfills/http2.ts @@ -79,6 +79,7 @@ const { } = primordials; const kSession = Symbol("session"); +const kOptions = Symbol("options"); const kAlpnProtocol = Symbol("alpnProtocol"); const kAuthority = Symbol("authority"); const kEncrypted = Symbol("encrypted"); @@ -726,7 +727,7 @@ export class Http2Stream extends EventEmitter { } sendTrailers(headers: Record) { - let request = toInnerRequest(this._request); + const request = toInnerRequest(this._request); op_http_set_response_trailers(request.external, Object.entries(headers)); } } @@ -1376,8 +1377,6 @@ export class ServerHttp2Stream extends Http2Stream { } } -const kOptions = Symbol("options"); - function setupCompat(ev) { if (ev === "request") { this.removeListener("newListener", setupCompat); @@ -1398,17 +1397,16 @@ function onServerStream( ServerResponse, stream, headers, - flags, + _flags, rawHeaders, ) { - const server = this; const request = new ServerRequest(stream, headers, undefined, rawHeaders); const response = new ServerResponse(stream); // Check for the CONNECT method const method = headers[constants.HTTP2_HEADER_METHOD]; if (method === "CONNECT") { - if (!server.emit("connect", request, response)) { + if (!this.emit("connect", request, response)) { response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED; response.end(); } @@ -1418,14 +1416,14 @@ function onServerStream( // Check for Expectations if (headers.expect !== undefined) { if (headers.expect === "100-continue") { - if (server.listenerCount("checkContinue")) { - server.emit("checkContinue", request, response); + if (this.listenerCount("checkContinue")) { + this.emit("checkContinue", request, response); } else { response.writeContinue(); - server.emit("request", request, response); + this.emit("request", request, response); } - } else if (server.listenerCount("checkExpectation")) { - server.emit("checkExpectation", request, response); + } else if (this.listenerCount("checkExpectation")) { + this.emit("checkExpectation", request, response); } else { response.statusCode = constants.HTTP_STATUS_EXPECTATION_FAILED; response.end(); @@ -1433,7 +1431,7 @@ function onServerStream( return; } - server.emit("request", request, response); + this.emit("request", request, response); } function initializeOptions(options) { @@ -2044,7 +2042,6 @@ const kSocket = Symbol("socket"); const kTrailers = Symbol("trailers"); const kRawTrailers = Symbol("rawTrailers"); const kSetHeader = Symbol("setHeader"); -const kServer = Symbol("server"); const kAppendHeader = Symbol("appendHeader"); const kAborted = Symbol("aborted"); const kProxySocket = Symbol("proxySocket"); @@ -2314,7 +2311,7 @@ function onStreamEnd() { } } -function onStreamError(error) { +function onStreamError(_error) { // This is purposefully left blank // // errors in compatibility mode are @@ -2378,7 +2375,6 @@ function onStreamAbortedResponse() { } let statusMessageWarned = false; -let statusConnectionHeaderWarned = false; // Defines and implements an API compatibility layer on top of the core // HTTP/2 implementation, intended to provide an interface that is as @@ -2479,10 +2475,6 @@ class Http2ServerResponse extends Stream { this[kState].sendDate = Boolean(bool); } - get statusCode() { - return this[kState].statusCode; - } - get writableCorked() { return this[kStream].writableCorked; } @@ -2499,6 +2491,10 @@ class Http2ServerResponse extends Stream { return this[kStream].writableLength; } + get statusCode() { + return this[kState].statusCode; + } + set statusCode(code) { code |= 0; if (code >= 100 && code < 200) { @@ -2855,7 +2851,6 @@ class Http2ServerResponse extends Stream { this[kStream].respond(headers, options); } - // TODO doesn't support callbacks writeContinue() { const stream = this[kStream]; if (stream.headersSent || this[kState].closed) { diff --git a/tests/util/test_util.ts b/tests/util/test_util.ts index 12ead9455c0b4d..ba9bf1839a6fdc 100644 --- a/tests/util/test_util.ts +++ b/tests/util/test_util.ts @@ -109,7 +109,7 @@ export async function curlRequestWithStdErr(args: string[]) { stdout: "piped", stderr: "piped", }).output(); - let decoder = new TextDecoder(); + const decoder = new TextDecoder(); assert( success, `Failed to cURL ${args}: stdout\n\n${ From 9481c0fd5836dc586f6788ae1474159cd93d9789 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Wed, 6 Mar 2024 13:56:48 +0530 Subject: [PATCH 08/15] replace old http2 test with new one --- tests/unit_node/http2_test.ts | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index a37d0991f319f8..0c9d73f829bd6c 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -109,35 +109,6 @@ Deno.test(`[node/http2 client createConnection]`, { assertEquals(receivedData, "hello world\n"); }); -// TODO(bartlomieju): reenable sanitizers -Deno.test("[node/http2 server]", { sanitizeOps: false }, async () => { - const server = http2.createServer(); - server.listen(0); - const port = ( server.address()).port; - const sessionPromise = new Promise((resolve) => - server.on("session", resolve) - ); - - const responsePromise = fetch(`http://localhost:${port}/path`, { - method: "POST", - body: "body", - }); - - const session = await sessionPromise; - const stream = await new Promise((resolve) => - session.on("stream", resolve) - ); - await new Promise((resolve) => stream.on("headers", resolve)); - await new Promise((resolve) => stream.on("data", resolve)); - await new Promise((resolve) => stream.on("end", resolve)); - stream.respond(); - stream.end(); - const resp = await responsePromise; - await resp.text(); - - await new Promise((resolve) => server.close(resolve)); -}); - Deno.test("[node/http2 client GET https://www.example.com]", async () => { const clientSession = http2.connect("https://www.example.com"); const req = clientSession.request({ @@ -167,7 +138,7 @@ Deno.test("[node/http2 client GET https://www.example.com]", async () => { assert(chunk.length > 0); }); -Deno.test("[node/http2.createServer()]", { sanitizeOps: false }, async () => { +Deno.test("[node/http2.createServer()]", async () => { const server = http2.createServer((_req, res) => { res.setHeader("Content-Type", "text/html"); res.setHeader("X-Foo", "bar"); From 898b0682c5570804fa96b33f4c8b6528967f9fba Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Wed, 6 Mar 2024 15:00:14 +0530 Subject: [PATCH 09/15] Revert "fix: update imports of test_util.ts" This reverts commit 6f0b85555d62417b384d9d137047c45576ac747e. --- tests/unit/files_test.ts | 2 +- tests/unit/real_path_test.ts | 2 +- tests/unit_node/dgram_test.ts | 2 +- tests/unit_node/fs_test.ts | 2 +- tests/unit_node/http2_test.ts | 2 +- tests/unit_node/http_test.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/files_test.ts b/tests/unit/files_test.ts index 57a2bb305c1fc6..0034a847222017 100644 --- a/tests/unit/files_test.ts +++ b/tests/unit/files_test.ts @@ -7,7 +7,7 @@ import { assertEquals, assertRejects, assertThrows, -} from "../util/test_util.ts"; +} from "./test_util.ts"; import { copy } from "@std/streams/copy.ts"; // Note tests for Deno.FsFile.setRaw is in integration tests. diff --git a/tests/unit/real_path_test.ts b/tests/unit/real_path_test.ts index 30a9b62016e112..b3656a927cd5db 100644 --- a/tests/unit/real_path_test.ts +++ b/tests/unit/real_path_test.ts @@ -6,7 +6,7 @@ import { assertRejects, assertThrows, pathToAbsoluteFileUrl, -} from "../util/test_util.ts"; +} from "./test_util.ts"; Deno.test({ permissions: { read: true } }, function realPathSyncSuccess() { const relative = "tests/testdata/assets/fixture.json"; diff --git a/tests/unit_node/dgram_test.ts b/tests/unit_node/dgram_test.ts index 425649d5222205..10c8e52537dec5 100644 --- a/tests/unit_node/dgram_test.ts +++ b/tests/unit_node/dgram_test.ts @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertEquals } from "@std/assert/mod.ts"; -import { execCode } from "../util/test_util.ts"; +import { execCode } from "../unit/test_util.ts"; import { createSocket } from "node:dgram"; const listenPort = 4503; diff --git a/tests/unit_node/fs_test.ts b/tests/unit_node/fs_test.ts index 3454e95d4a8e2d..4a8ef89f3da0f7 100644 --- a/tests/unit_node/fs_test.ts +++ b/tests/unit_node/fs_test.ts @@ -12,7 +12,7 @@ import { writeFileSync, } from "node:fs"; import { constants as fsPromiseConstants, cp } from "node:fs/promises"; -import { pathToAbsoluteFileUrl } from "../util/test_util.ts"; +import { pathToAbsoluteFileUrl } from "../unit/test_util.ts"; Deno.test( "[node/fs writeFileSync] write file without option", diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index 0c9d73f829bd6c..bc7ababef93a01 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -3,7 +3,7 @@ import * as http2 from "node:http2"; import * as net from "node:net"; import { assert, assertEquals } from "@std/assert/mod.ts"; -import { curlRequest } from "../util/test_util.ts"; +import { curlRequest } from "../unit/test_util.ts"; for (const url of ["http://127.0.0.1:4246", "https://127.0.0.1:4247"]) { Deno.test(`[node/http2 client] ${url}`, { diff --git a/tests/unit_node/http_test.ts b/tests/unit_node/http_test.ts index db0635e136530e..57ade6298a589a 100644 --- a/tests/unit_node/http_test.ts +++ b/tests/unit_node/http_test.ts @@ -10,7 +10,7 @@ import { assertSpyCalls, spy } from "@std/testing/mock.ts"; import { gzip } from "node:zlib"; import { Buffer } from "node:buffer"; import { serve } from "@std/http/server.ts"; -import { execCode } from "../util/test_util.ts"; +import { execCode } from "../unit/test_util.ts"; Deno.test("[node/http listen]", async () => { { From 8b76574ddcec8e64db6e9ab1f855767882803992 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Wed, 6 Mar 2024 15:00:38 +0530 Subject: [PATCH 10/15] Revert "refactor: move test_util.ts" This reverts commit cdaa729a08b404e57faa8bb55bba3eb715be1913. --- tests/unit/chmod_test.ts | 2 +- tests/unit/command_test.ts | 2 +- tests/unit/read_file_test.ts | 2 +- tests/unit/serve_test.ts | 2 +- tests/unit/symlink_test.ts | 2 +- tests/{util => unit}/test_util.ts | 0 tests/unit/tls_test.ts | 2 +- tests/unit/write_file_test.ts | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename tests/{util => unit}/test_util.ts (100%) diff --git a/tests/unit/chmod_test.ts b/tests/unit/chmod_test.ts index 5e22d6b60a7eaf..df3771bbc1631d 100644 --- a/tests/unit/chmod_test.ts +++ b/tests/unit/chmod_test.ts @@ -4,7 +4,7 @@ import { assertEquals, assertRejects, assertThrows, -} from "../util/test_util.ts"; +} from "./test_util.ts"; Deno.test( { diff --git a/tests/unit/command_test.ts b/tests/unit/command_test.ts index ac540ef26cb97c..cbb1c4921c9278 100644 --- a/tests/unit/command_test.ts +++ b/tests/unit/command_test.ts @@ -6,7 +6,7 @@ import { assertRejects, assertStringIncludes, assertThrows, -} from "../util/test_util.ts"; +} from "./test_util.ts"; Deno.test( { permissions: { write: true, run: true, read: true } }, diff --git a/tests/unit/read_file_test.ts b/tests/unit/read_file_test.ts index 8b34cc58f09645..bfb3b508512887 100644 --- a/tests/unit/read_file_test.ts +++ b/tests/unit/read_file_test.ts @@ -6,7 +6,7 @@ import { assertThrows, pathToAbsoluteFileUrl, unreachable, -} from "../util/test_util.ts"; +} from "./test_util.ts"; Deno.test({ permissions: { read: true } }, function readFileSyncSuccess() { const data = Deno.readFileSync("tests/testdata/assets/fixture.json"); diff --git a/tests/unit/serve_test.ts b/tests/unit/serve_test.ts index 02d549732f816c..a88870bc7ba51a 100644 --- a/tests/unit/serve_test.ts +++ b/tests/unit/serve_test.ts @@ -13,7 +13,7 @@ import { execCode, fail, tmpUnixSocketPath, -} from "../util/test_util.ts"; +} from "./test_util.ts"; // Since these tests may run in parallel, ensure this port is unique to this file const servePort = 4502; diff --git a/tests/unit/symlink_test.ts b/tests/unit/symlink_test.ts index f8498747603ee0..310c3693055d73 100644 --- a/tests/unit/symlink_test.ts +++ b/tests/unit/symlink_test.ts @@ -4,7 +4,7 @@ import { assertRejects, assertThrows, pathToAbsoluteFileUrl, -} from "../util/test_util.ts"; +} from "./test_util.ts"; Deno.test( { permissions: { read: true, write: true } }, diff --git a/tests/util/test_util.ts b/tests/unit/test_util.ts similarity index 100% rename from tests/util/test_util.ts rename to tests/unit/test_util.ts diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index ea4e8c43f27eb6..1ac5e8d9832c59 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -6,7 +6,7 @@ import { assertRejects, assertStrictEquals, assertThrows, -} from "../util/test_util.ts"; +} from "./test_util.ts"; import { BufReader, BufWriter } from "@std/io/mod.ts"; import { readAll } from "@std/streams/read_all.ts"; import { writeAll } from "@std/streams/write_all.ts"; diff --git a/tests/unit/write_file_test.ts b/tests/unit/write_file_test.ts index 1ba526ca62a1b1..6cd08e2d14db98 100644 --- a/tests/unit/write_file_test.ts +++ b/tests/unit/write_file_test.ts @@ -5,7 +5,7 @@ import { assertRejects, assertThrows, unreachable, -} from "../util/test_util.ts"; +} from "./test_util.ts"; Deno.test( { permissions: { read: true, write: true } }, From 6de9e5a1687f1920a471414f1b475c857374606f Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Wed, 6 Mar 2024 15:07:30 +0530 Subject: [PATCH 11/15] revert unrelated changes Signed-off-by: Satya Rohith --- tests/unit/serve_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/serve_test.ts b/tests/unit/serve_test.ts index a88870bc7ba51a..9b2870ebd112c9 100644 --- a/tests/unit/serve_test.ts +++ b/tests/unit/serve_test.ts @@ -3710,7 +3710,7 @@ Deno.test( // TODO(mmastrac): This test should eventually use fetch, when we support trailers there. // This test is ignored because it's flaky and relies on cURL's verbose output. Deno.test( - { permissions: { net: true, run: true, read: true }, ignore: false }, + { permissions: { net: true, run: true, read: true }, ignore: true }, async function httpServerTrailers() { const ac = new AbortController(); const { resolve } = Promise.withResolvers(); From 93377baf9c201d6bea8717e96bf6dcf4f986f88c Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Wed, 6 Mar 2024 18:48:08 +0530 Subject: [PATCH 12/15] make the test less flaky --- tests/unit_node/http2_test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index bc7ababef93a01..8e7a525da9455c 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -156,4 +156,7 @@ Deno.test("[node/http2.createServer()]", async () => { ]); assertEquals(response, "Hello, World!"); server.close(); + // Wait to avoid leaking the timer from here + // https://github.com/denoland/deno/blob/749b6e45e58ac87188027f79fe403d130f86bd73/ext/node/polyfills/net.ts#L2396-L2402 + await new Promise((resolve) => server.on("close", resolve)); }); From 9c79c7e9a1c4674b44fb1ec6a290ca3efa422fb4 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Wed, 6 Mar 2024 22:40:34 +0530 Subject: [PATCH 13/15] try sleep(20ms) --- tests/unit_node/http2_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index 8e7a525da9455c..c76e38f6fc04be 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -158,5 +158,5 @@ Deno.test("[node/http2.createServer()]", async () => { server.close(); // Wait to avoid leaking the timer from here // https://github.com/denoland/deno/blob/749b6e45e58ac87188027f79fe403d130f86bd73/ext/node/polyfills/net.ts#L2396-L2402 - await new Promise((resolve) => server.on("close", resolve)); + await new Promise((resolve) => setTimeout(resolve, 20)); }); From 7e9004e2783f3dc78eb77004ebcc9b9dd98b3f3b Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Thu, 7 Mar 2024 11:08:41 +0530 Subject: [PATCH 14/15] Revert "try sleep(20ms)" This reverts commit 9c79c7e9a1c4674b44fb1ec6a290ca3efa422fb4. --- tests/unit_node/http2_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index c76e38f6fc04be..8e7a525da9455c 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -158,5 +158,5 @@ Deno.test("[node/http2.createServer()]", async () => { server.close(); // Wait to avoid leaking the timer from here // https://github.com/denoland/deno/blob/749b6e45e58ac87188027f79fe403d130f86bd73/ext/node/polyfills/net.ts#L2396-L2402 - await new Promise((resolve) => setTimeout(resolve, 20)); + await new Promise((resolve) => server.on("close", resolve)); }); From 81de1872b1d9d1949f37c03dacfaa31ab03e23c6 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Thu, 7 Mar 2024 11:26:02 +0530 Subject: [PATCH 15/15] ignore windows --- tests/unit_node/http2_test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index 8e7a525da9455c..872e6641e29ca4 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -138,7 +138,10 @@ Deno.test("[node/http2 client GET https://www.example.com]", async () => { assert(chunk.length > 0); }); -Deno.test("[node/http2.createServer()]", async () => { +Deno.test("[node/http2.createServer()]", { + // TODO(satyarohith): enable the test on windows. + ignore: Deno.build.os === "windows", +}, async () => { const server = http2.createServer((_req, res) => { res.setHeader("Content-Type", "text/html"); res.setHeader("X-Foo", "bar"); @@ -158,5 +161,6 @@ Deno.test("[node/http2.createServer()]", async () => { server.close(); // Wait to avoid leaking the timer from here // https://github.com/denoland/deno/blob/749b6e45e58ac87188027f79fe403d130f86bd73/ext/node/polyfills/net.ts#L2396-L2402 + // Issue: https://github.com/denoland/deno/issues/22764 await new Promise((resolve) => server.on("close", resolve)); });