1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00

fix(ext/node) add node http methods (#22630)

fixes #22627

This PR fixes a node compat issue that is preventing `serverless-http`
and `serverless-express` npm modules from working with Deno. These
modules are useful for running apps on AWS Lambda (and other serverless
infra).

---------

Signed-off-by: Igor Zinkovsky <igor@deno.com>
This commit is contained in:
Igor Zinkovsky 2024-02-29 14:56:04 -08:00 committed by GitHub
parent 627c49c9d8
commit dc16c996dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 107 additions and 1 deletions

View file

@ -51,6 +51,7 @@ import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts";
import { import {
connResetException, connResetException,
ERR_HTTP_HEADERS_SENT, ERR_HTTP_HEADERS_SENT,
ERR_HTTP_SOCKET_ASSIGNED,
ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_TYPE,
ERR_INVALID_HTTP_TOKEN, ERR_INVALID_HTTP_TOKEN,
ERR_INVALID_PROTOCOL, ERR_INVALID_PROTOCOL,
@ -1340,6 +1341,8 @@ export class ServerResponse extends NodeWritable {
headersSent = false; headersSent = false;
#firstChunk: Chunk | null = null; #firstChunk: Chunk | null = null;
#resolve: (value: Response | PromiseLike<Response>) => void; #resolve: (value: Response | PromiseLike<Response>) => void;
// deno-lint-ignore no-explicit-any
#socketOverride: any | null = null;
static #enqueue(controller: ReadableStreamDefaultController, chunk: Chunk) { static #enqueue(controller: ReadableStreamDefaultController, chunk: Chunk) {
if (typeof chunk === "string") { if (typeof chunk === "string") {
@ -1369,7 +1372,11 @@ export class ServerResponse extends NodeWritable {
autoDestroy: true, autoDestroy: true,
defaultEncoding: "utf-8", defaultEncoding: "utf-8",
emitClose: true, emitClose: true,
write: (chunk, _encoding, cb) => { write: (chunk, encoding, cb) => {
if (this.#socketOverride && this.#socketOverride.writable) {
this.#socketOverride.write(chunk, encoding);
return cb();
}
if (!this.headersSent) { if (!this.headersSent) {
if (this.#firstChunk === null) { if (this.#firstChunk === null) {
this.#firstChunk = chunk; this.#firstChunk = chunk;
@ -1418,6 +1425,9 @@ export class ServerResponse extends NodeWritable {
getHeaderNames() { getHeaderNames() {
return Array.from(this.#headers.keys()); return Array.from(this.#headers.keys());
} }
getHeaders() {
return Object.fromEntries(this.#headers.entries());
}
hasHeader(name: string) { hasHeader(name: string) {
return this.#headers.has(name); return this.#headers.has(name);
} }
@ -1483,6 +1493,20 @@ export class ServerResponse extends NodeWritable {
_implicitHeader() { _implicitHeader() {
this.writeHead(this.statusCode); this.writeHead(this.statusCode);
} }
assignSocket(socket) {
if (socket._httpMessage) {
throw new ERR_HTTP_SOCKET_ASSIGNED();
}
socket._httpMessage = this;
this.#socketOverride = socket;
}
detachSocket(socket) {
assert(socket._httpMessage === this);
socket._httpMessage = null;
this.#socketOverride = null;
}
} }
// TODO(@AaronO): optimize // TODO(@AaronO): optimize
@ -1534,6 +1558,10 @@ export class IncomingMessageForServer extends NodeReadable {
return "1.1"; return "1.1";
} }
set httpVersion(val) {
assert(val === "1.1");
}
get headers() { get headers() {
if (!this.#headers) { if (!this.#headers) {
this.#headers = {}; this.#headers = {};
@ -1546,6 +1574,10 @@ export class IncomingMessageForServer extends NodeReadable {
return this.#headers; return this.#headers;
} }
set headers(val) {
this.#headers = val;
}
get upgrade(): boolean { get upgrade(): boolean {
return Boolean( return Boolean(
this.#req.headers.get("connection")?.toLowerCase().includes("upgrade") && this.#req.headers.get("connection")?.toLowerCase().includes("upgrade") &&

View file

@ -2543,6 +2543,15 @@ export class ERR_OS_NO_HOMEDIR extends NodeSystemError {
} }
} }
export class ERR_HTTP_SOCKET_ASSIGNED extends NodeError {
constructor() {
super(
"ERR_HTTP_SOCKET_ASSIGNED",
`ServerResponse has an already assigned socket`,
);
}
}
interface UvExceptionContext { interface UvExceptionContext {
syscall: string; syscall: string;
path?: string; path?: string;

View file

@ -3,6 +3,7 @@
import EventEmitter from "node:events"; import EventEmitter from "node:events";
import http, { type RequestOptions } from "node:http"; import http, { type RequestOptions } from "node:http";
import https from "node:https"; import https from "node:https";
import net from "node:net";
import { assert, assertEquals, fail } from "@std/assert/mod.ts"; import { assert, assertEquals, fail } from "@std/assert/mod.ts";
import { assertSpyCalls, spy } from "@std/testing/mock.ts"; import { assertSpyCalls, spy } from "@std/testing/mock.ts";
@ -938,3 +939,67 @@ Deno.test("[node/http] ServerResponse getHeader", async () => {
await promise; await promise;
}); });
Deno.test("[node/http] IncomingMessage override", () => {
const req = new http.IncomingMessage(new net.Socket());
// https://github.com/dougmoscrop/serverless-http/blob/3aaa6d0fe241109a8752efb011c242d249f32368/lib/request.js#L20-L30
Object.assign(req, {
ip: "1.1.1.1",
complete: true,
httpVersion: "1.1",
httpVersionMajor: "1",
httpVersionMinor: "1",
method: "GET",
headers: {},
body: "",
url: "https://1.1.1.1",
});
});
Deno.test("[node/http] ServerResponse assignSocket and detachSocket", () => {
const req = new http.IncomingMessage(new net.Socket());
const res = new http.ServerResponse(req);
let writtenData: string | Uint8Array | undefined = undefined;
let writtenEncoding: string | Uint8Array | undefined = undefined;
const socket = {
_writableState: {},
writable: true,
on: Function.prototype,
removeListener: Function.prototype,
destroy: Function.prototype,
cork: Function.prototype,
uncork: Function.prototype,
write: (
data: string | Uint8Array,
encoding: string,
_cb?: (err?: Error) => void,
) => {
writtenData = data;
writtenEncoding = encoding;
},
};
// @ts-ignore it's a socket mock
res.assignSocket(socket);
res.write("Hello World!", "utf8");
assertEquals(writtenData, Buffer.from("Hello World!"));
assertEquals(writtenEncoding, "buffer");
writtenData = undefined;
writtenEncoding = undefined;
// @ts-ignore it's a socket mock
res.detachSocket(socket);
res.write("Hello World!", "utf8");
assertEquals(writtenData, undefined);
assertEquals(writtenEncoding, undefined);
});
Deno.test("[node/http] ServerResponse getHeaders", () => {
const req = new http.IncomingMessage(new net.Socket());
const res = new http.ServerResponse(req);
res.setHeader("foo", "bar");
res.setHeader("bar", "baz");
assertEquals(res.getHeaderNames(), ["bar", "foo"]);
assertEquals(res.getHeaders(), { "bar": "baz", "foo": "bar" });
});