Skip to content
Merged
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
58 changes: 57 additions & 1 deletion src/Server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import http from "node:http";
import packageJson from "../package.json" with {type: "json"};
import {Request} from "./Request.js";
import {EmptyResponse} from "./response/index.js";
import {Response} from "./response/Response.js";
import {RouteRegistry} from "./routing/RouteRegistry.js";
import {ServerErrorRegistry} from "./ServerErrorRegistry.js";
Expand All @@ -16,6 +17,7 @@ class Server {
public readonly routes = new RouteRegistry();
private readonly server: http.Server;
private readonly copyOrigin: boolean;
private readonly handleConditionalRequests: boolean;

/**
* This server's error registry.
Expand All @@ -36,6 +38,7 @@ class Server {
this.globalHeaders.set("Server", `cldn/${packageJson.version}`);

this.copyOrigin = options.copyOrigin ?? false;
this.handleConditionalRequests = options.handleConditionalRequests ?? true;

this.server.listen(options.port);
}
Expand Down Expand Up @@ -80,7 +83,53 @@ class Server {
response = this.errors._get(ServerErrorRegistry.ErrorCodes.INTERNAL, apiRequest);
}
}
response._send(res, this, apiRequest);
await this.sendResponse(response, res, apiRequest);
}

private async sendResponse(response: Response, res: http.ServerResponse, req: Request): Promise<void> {
conditional: if (
this.handleConditionalRequests
&& response.statusCode === 200
&& [Request.Method.GET, Request.Method.HEAD].includes(req.method)
) {
const responseHeaders = response.allHeaders(res, this, req);
const etag = responseHeaders.get("etag");
const lastModified = responseHeaders.has("last-modified")
? new Date(responseHeaders.get("last-modified")!)
: null;
if (etag === null && lastModified === null)
break conditional;

if (req.headers.has("if-match")) {
if (!this.getETags(req.headers.get("if-match")!)
.filter(t => !t.startsWith("W/"))
.includes(etag!))
return this.errors._get(ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED, req)._send(res, this, req);
}
else if (req.headers.has("if-unmodified-since")) {
if (lastModified === null
|| lastModified.getTime() > new Date(req.headers.get("if-unmodified-since")!).getTime())
return this.errors._get(ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED, req)._send(res, this, req);
}

if (req.headers.has("if-none-match")) {
if (this.getETags(req.headers.get("if-none-match")!)
.includes(etag!))
return new EmptyResponse(responseHeaders, 304)._send(res, this, req);
}
else if (req.headers.has("if-modified-since")) {
if (lastModified !== null
&& lastModified.getTime() <= new Date(req.headers.get("if-modified-since")!).getTime())
return new EmptyResponse(responseHeaders, 304)._send(res, this, req);
}
}
response._send(res, this, req);
}

private getETags(header: string) {
return header
.split(",")
.map(t => t.trim())
}

public close(): Promise<void> {
Expand Down Expand Up @@ -119,6 +168,13 @@ namespace Server {
* @default false
*/
readonly copyOrigin?: boolean;

/**
* Automatically handle conditional requests for GET and HEAD requests that result in a 200 status code.
* `If-Range` headers are ignored.
* @default true
*/
readonly handleConditionalRequests?: boolean;
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/ServerErrorRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class ServerErrorRegistry {

[ServerErrorRegistry.ErrorCodes.INTERNAL]:
new TextResponse("An internal error occurred.", 500),

[ServerErrorRegistry.ErrorCodes.PRECONDITION_FAILED]:
new TextResponse("Precondition failed.", 412),
};
}

Expand Down Expand Up @@ -49,6 +52,7 @@ namespace ServerErrorRegistry {
BAD_URL,
NO_ROUTE,
INTERNAL,
PRECONDITION_FAILED,
}
}

Expand Down
15 changes: 12 additions & 3 deletions src/response/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export abstract class Response {
/**
* The HTTP response status code to send.
*/
protected readonly statusCode: number;
public readonly statusCode: number;

/**
* The HTTP response headers to send.
Expand Down Expand Up @@ -43,9 +43,10 @@ export abstract class Response {
}

/**
* Set the HTTP response status code and headers.
* All (final) headers to send to the client.
* @internal
*/
protected writeHead(res: http.ServerResponse, server: Server, req?: Request) {
public allHeaders(res: http.ServerResponse, server: Server, req?: Request) {
const headers = new Headers(this.headers);
if (req !== undefined)
for (const [key, value] of req._responseHeaders)
Expand All @@ -62,6 +63,14 @@ export abstract class Response {
headers.set("connection", "keep-alive");
headers.set("keep-alive", "timeout=" + server._keepAliveTimeout);
}
return headers;
}

/**
* Set the HTTP response status code and headers.
*/
protected writeHead(res: http.ServerResponse, server: Server, req?: Request) {
const headers = this.allHeaders(res, server, req);
for (const [key, value] of Array.from(headers.entries())
.sort((a, b) => a[0].localeCompare(b[0])))
res.setHeader(key, value);
Expand Down