Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(std/http): add HTTP server "error" event #703

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions http/file_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import { extname, posix } from "../path/mod.ts";
import {
HTTPSOptions,
listenAndServe,
listenAndServeTLS,
Response,
serve,
Server,
ServerRequest,
serveTLS,
} from "./server.ts";
import { parse } from "../flags/mod.ts";
import { assert } from "../_util/assert.ts";
Expand Down Expand Up @@ -375,6 +376,16 @@ function normalizeURL(url: string): string {
: normalizedUrl;
}

function addrToString(addr: Deno.Addr) {
assert(addr.transport == "tcp");
if (addr.hostname.indexOf(":") >= 0 && addr.hostname[0] != "[") {
// Print "[ip]:port" for IPv6 addresses.
return `[${addr.hostname}]:${addr.port}`;
} else {
return `${addr.hostname}:${addr.port}`;
}
}

function main(): void {
const CORSEnabled = serverArgs.cors ? true : false;
const port = serverArgs.port ?? serverArgs.p ?? 4507;
Expand Down Expand Up @@ -451,15 +462,36 @@ function main(): void {
}
};

let server: Server;
let proto = "http";
if (tlsOpts.keyFile || tlsOpts.certFile) {
proto += "s";
tlsOpts.hostname = host;
tlsOpts.port = port;
listenAndServeTLS(tlsOpts, handler);
server = serveTLS(tlsOpts);
} else {
listenAndServe(addr, handler);
server = serve(addr);
}

server.addEventListener("error", (evt) => {
if (evt.origin == "request") {
console.error(
`Error occurred while reading the request from ${
addrToString(evt.connection!.remoteAddr)
}:`,
evt.error,
);
} else {
console.error(evt.error);
}
});

(async () => {
for await (const req of server) {
handler(req);
}
})();

console.log(`${proto.toUpperCase()} server listening on ${proto}://${addr}/`);
}

Expand Down
104 changes: 86 additions & 18 deletions http/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,58 @@ export class ServerRequest {
}
}

export class Server implements AsyncIterable<ServerRequest> {
export type ServerErrorEventListenerOrEventListenerObject =
| ServerErrorListener
| ServerErrorListenerObject;

export interface ServerErrorListener {
(evt: ServerErrorEvent): void | Promise<void>;
}

export interface ServerErrorListenerObject {
handleEvent(evt: ServerErrorEvent): void | Promise<void>;
}

export interface ServerErrorEventInit extends ErrorEventInit {
server: Server;
origin: ServerErrorEvent["origin"];
connection?: Deno.Conn;
}

export class ServerErrorEvent extends ErrorEvent {
server: Server;
origin: "listener" | "request";
connection?: Deno.Conn;
constructor(init: ServerErrorEventInit) {
super("error", init);
this.server = init.server;
this.origin = init.origin;
this.connection = init.connection;
}
}

export class Server extends EventTarget
implements AsyncIterable<ServerRequest> {
#closing = false;
#connections: Deno.Conn[] = [];

constructor(public listener: Deno.Listener) {}
constructor(public listener: Deno.Listener) {
super();
}

addEventListener(
type: "error",
listener: ServerErrorEventListenerOrEventListenerObject | null,
options?: boolean | AddEventListenerOptions,
): void;

addEventListener(
type: "error",
listener: EventListenerOrEventListenerObject | null,
options?: boolean | AddEventListenerOptions,
): void {
super.addEventListener(type, listener, options);
}

close(): void {
this.#closing = true;
Expand All @@ -142,15 +189,27 @@ export class Server implements AsyncIterable<ServerRequest> {
): AsyncIterableIterator<ServerRequest> {
const reader = new BufReader(conn);
const writer = new BufWriter(conn);
let preventClosing = false;

while (!this.#closing) {
let request: ServerRequest | null;
try {
request = await readRequest(conn, reader);
} catch (error) {
const eventDefault = this.dispatchEvent(
new ServerErrorEvent({
error,
origin: "request",
server: this,
connection: conn,
cancelable: true,
}),
);
if (
error instanceof Deno.errors.InvalidData ||
error instanceof Deno.errors.UnexpectedEof
eventDefault && (
error instanceof Deno.errors.InvalidData ||
error instanceof Deno.errors.UnexpectedEof
)
) {
// An error was thrown while parsing request headers.
// Try to send the "400 Bad Request" before closing the connection.
Expand All @@ -163,6 +222,9 @@ export class Server implements AsyncIterable<ServerRequest> {
// The connection is broken.
}
}
if (!eventDefault) {
preventClosing = true;
}
break;
}
if (request === null) {
Expand Down Expand Up @@ -193,10 +255,12 @@ export class Server implements AsyncIterable<ServerRequest> {
}

this.untrackConnection(conn);
try {
conn.close();
} catch {
// might have been already closed
if (!preventClosing) {
try {
conn.close();
} catch {
// might have been already closed
}
}
}

Expand Down Expand Up @@ -224,17 +288,21 @@ export class Server implements AsyncIterable<ServerRequest> {
try {
conn = await this.listener.accept();
} catch (error) {
if (
// The listener is closed:
error instanceof Deno.errors.BadResource ||
// TLS handshake errors:
error instanceof Deno.errors.InvalidData ||
error instanceof Deno.errors.UnexpectedEof ||
error instanceof Deno.errors.ConnectionReset
) {
return mux.add(this.acceptConnAndIterateHttpRequests(mux));
if (this.#closing && error instanceof Deno.errors.BadResource) {
return;
}
const eventDefault = this.dispatchEvent(
new ServerErrorEvent({
error,
origin: "listener",
server: this,
cancelable: true,
}),
);
if (eventDefault) {
mux.add(this.acceptConnAndIterateHttpRequests(mux));
}
throw error;
return;
}
this.trackConnection(conn);
// Try to accept another connection and add it to the multiplexer.
Expand Down
76 changes: 76 additions & 0 deletions http/server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,82 @@ Deno.test({
},
});

Deno.test({
name: "[http] request error event",
async fn(): Promise<void> {
const server = serve(":8124");
let eventTriggered = false;
server.addEventListener("error", (evt) => {
if (
evt.origin == "request" &&
evt.error instanceof Deno.errors.InvalidData
) {
eventTriggered = true;
// Call `preventDefault` to avoid sending "400 Bad Request" and closing the connection
evt.preventDefault();
(async () => {
assert(!!evt.connection);
await writeAll(evt.connection, new TextEncoder().encode("bad!"));
evt.connection.close();
})();
} else {
throw new Error("Unexpected error event: " + Deno.inspect(evt));
}
});
const entry = server[Symbol.asyncIterator]().next();
const conn = await Deno.connect({
hostname: "127.0.0.1",
port: 8124,
});
await writeAll(
conn,
new TextEncoder().encode(
"GET / HTTP/1.1\r\nmalformedHeader\r\n\r\n\r\n\r\n",
),
);
const responseString = new TextDecoder().decode(await readAll(conn));
assertEquals(responseString, "bad!");
conn.close();
server.close();
assert((await entry).done);
assert(eventTriggered);
},
});

Deno.test({
name: "[http] listener error event",
async fn(): Promise<void> {
const badListener = new Proxy(Deno.listen({ port: 8124 }), {
get(target, p) {
if (p == "accept") {
return () => {
throw new Error("Failed to accept connection");
};
}
return target[p as keyof Deno.Listener];
},
});
const server = new Server(badListener);
let eventTriggered = false;
server.addEventListener("error", (evt) => {
if (
evt.origin == "listener" &&
evt.error.message == "Failed to accept connection"
) {
// Call `preventDefault` to stop accepting connections
evt.preventDefault();
eventTriggered = true;
} else {
throw new Error("Unexpected error event: " + Deno.inspect(evt));
}
});
const entry = server[Symbol.asyncIterator]().next();
assert((await entry).done);
assert(eventTriggered);
server.close();
},
});

Deno.test({
name: "[http] finalizing invalid chunked data closes connection",
async fn() {
Expand Down