Skip to content

Commit

Permalink
better headers handling
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Jun 23, 2024
1 parent d821c33 commit 8685170
Show file tree
Hide file tree
Showing 29 changed files with 2,691 additions and 436 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ lib
.nuxt
.output
docs/**/*.md
src/types/_headers.ts
2 changes: 1 addition & 1 deletion docs/2.utils/1.request.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ Get a unique fingerprint for the incoming request.

<!-- automd:jsdocs src="../../src/utils/body.ts" -->

### `readBodyStream(event)`
### `getBodyStream(event)`

Captures a stream from a request.

Expand Down
3 changes: 2 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export default unjs(
"unicorn/no-null": "off",
"unicorn/number-literal-case": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"unicorn/expiring-todo-comments": "off"
"unicorn/expiring-todo-comments": "off",
"@typescript-eslint/ban-types": "off"
}
}
);
96 changes: 96 additions & 0 deletions src/adapters/node/_headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { splitCookiesString } from "../../utils";
import type { OutgoingHttpHeaders, IncomingHttpHeaders } from "node:http";

type NodeHeaders = OutgoingHttpHeaders | IncomingHttpHeaders;

export class NodeHeadersProxy implements Headers {
getHeaders: () => NodeHeaders;

constructor(getHeaders: () => NodeHeaders) {
this.getHeaders = getHeaders;
}

append(name: string, value: string): void {
const _headers = this.getHeaders();
const _current = _headers[name];
if (_current) {
if (Array.isArray(_current)) {
_current.push(value);
} else {
_headers[name] = [_current as string, value];
}
} else {
_headers[name] = value;
}
}

delete(name: string): void {
this.getHeaders()[name] = undefined;
}

get(name: string): string | null {
return _normalizeValue(this.getHeaders()[name]);
}

getSetCookie(): string[] {
const setCookie = this.getHeaders()["set-cookie"];
if (!setCookie || setCookie.length === 0) {
return [];
}
return splitCookiesString(setCookie);
}

has(name: string): boolean {
return !!this.getHeaders()[name];
}

set(name: string, value: string): void {
this.getHeaders()[name] = value;
}

forEach(
cb: (value: string, key: string, parent: Headers) => void,
thisArg?: any,
): void {
const _headers = this.getHeaders();
for (const key in _headers) {
if (_headers[key]) {
cb.call(thisArg, _normalizeValue(_headers[key]), key, this);
}
}
}

*entries(): IterableIterator<[string, string]> {
const _headers = this.getHeaders();
for (const key in _headers) {
yield [key, _normalizeValue(_headers[key])];
}
}

*keys(): IterableIterator<string> {
const keys = Object.keys(this.getHeaders());
for (const key of keys) {
yield key;
}
}

*values(): IterableIterator<string> {
const values = Object.values(this.getHeaders());
for (const value of values) {
yield _normalizeValue(value);
}
}

[Symbol.iterator](): IterableIterator<[string, string]> {
return this.entries()[Symbol.iterator]();
}
}

function _normalizeValue(
value: string | string[] | number | undefined,
): string {
if (Array.isArray(value)) {
return value.join(", ");
}
return (value as string) || "";
}
2 changes: 1 addition & 1 deletion src/adapters/node/_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function _callNodeHandler(
});
}

export function _readBodyStream(
export function _getBodyStream(
req: NodeIncomingMessage,
): ReadableStream<Uint8Array> {
return new ReadableStream({
Expand Down
28 changes: 19 additions & 9 deletions src/adapters/node/event.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { HTTPMethod } from "../../types";
import { RawEvent, type RawResponse } from "../../types/_event";
import { splitCookiesString } from "../../utils";
import { NodeHeadersProxy } from "./_headers";
import {
_normalizeHeaders,
_readBody,
_readBodyStream,
_getBodyStream,
_sendResponse,
} from "./_internal";

Expand All @@ -24,6 +26,9 @@ export class NodeEvent implements RawEvent {
_formDataBody?: Promise<undefined | FormData>;
_bodyStream?: undefined | ReadableStream<Uint8Array>;

_headers?: NodeHeadersProxy;
_responseHeaders?: NodeHeadersProxy;

constructor(req: NodeIncomingMessage, res: NodeServerResponse) {
this._req = req;
this._res = res;
Expand Down Expand Up @@ -66,7 +71,10 @@ export class NodeEvent implements RawEvent {
}

getHeaders() {
return _normalizeHeaders(this._req.headers);
if (!this._headers) {
this._headers = new NodeHeadersProxy(() => this._req.headers);
}
return this._headers;
}

get remoteAddress() {
Expand Down Expand Up @@ -107,9 +115,9 @@ export class NodeEvent implements RawEvent {
return this._formDataBody;
}

readBodyStream() {
getBodyStream() {
if (!this._bodyStream) {
this._bodyStream = _readBodyStream(this._req);
this._bodyStream = _getBodyStream(this._req);
}
return this._bodyStream;
}
Expand Down Expand Up @@ -156,18 +164,20 @@ export class NodeEvent implements RawEvent {
}

getResponseHeaders() {
return _normalizeHeaders(this._res.getHeaders());
if (!this._responseHeaders) {
this._responseHeaders = new NodeHeadersProxy(() =>
this._res.getHeaders(),
);
}
return this._responseHeaders;
}

getResponseSetCookie() {
const value = this._res.getHeader("set-cookie");
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value;
}
return [value as string];
return splitCookiesString(value as string | string[]);
}

removeResponseHeader(key: string) {
Expand Down
14 changes: 5 additions & 9 deletions src/adapters/web/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export class WebEvent implements RawEvent {

_path?: string;
_originalPath?: string | undefined;
_headers?: Record<string, string>;

_responseBody?: RawResponse;
_responseCode?: number;
Expand Down Expand Up @@ -56,14 +55,11 @@ export class WebEvent implements RawEvent {
}

getHeader(key: string) {
return this._req.headers.get(key);
return this._req.headers.get(key) ?? undefined;
}

getHeaders() {
if (!this._headers) {
this._headers = Object.fromEntries(this._req.headers.entries());
}
return this._headers;
return this._req.headers;
}

get remoteAddress() {
Expand Down Expand Up @@ -105,7 +101,7 @@ export class WebEvent implements RawEvent {
return this._formDataBody;
}

readBodyStream() {
getBodyStream() {
return this._req.body || undefined;
}

Expand Down Expand Up @@ -140,11 +136,11 @@ export class WebEvent implements RawEvent {
}

getResponseHeader(key: string) {
return this._responseHeaders.get(key);
return this._responseHeaders.get(key) ?? undefined;
}

getResponseHeaders() {
return Object.fromEntries(this._responseHeaders.entries());
return this._responseHeaders;
}

getResponseSetCookie() {
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/web/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface PlainRequest {
export interface PlainResponse {
status: number;
statusText: string;
headers: [string, string][];
headers: Record<string, string>;
setCookie: string[];
body?: unknown;
}
42 changes: 34 additions & 8 deletions src/adapters/web/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function toWebRequest(event: H3Event): Request {
duplex: "half",
method: event[_kRaw].method,
headers: event[_kRaw].getHeaders(),
body: event[_kRaw].readBodyStream(),
body: event[_kRaw].getBodyStream(),
},
)
);
Expand Down Expand Up @@ -91,11 +91,18 @@ export function toPlainHandler(app: App) {
}),
context,
);

const setCookie = res.headers.getSetCookie();
const headersObject = Object.fromEntries(res.headers.entries());
if (setCookie.length > 0) {
headersObject["set-cookie"] = setCookie.join(", ");
}

return {
status: res.status,
statusText: res.statusText,
headers: [...res.headers.entries()],
setCookie: res.headers.getSetCookie(),
headers: headersObject,
setCookie: setCookie,
body: res.body,
};
};
Expand All @@ -109,18 +116,37 @@ export function fromPlainHandler(handler: PlainHandler) {
return defineEventHandler(async (event) => {
const res = await handler(
{
method: event.method,
path: event.path,
headers: event[_kRaw].getHeaders(),
body: undefined, // TODO
get method() {
return event.method;
},
get path() {
return event.path;
},
get headers() {
return event[_kRaw].getHeaders();
},
get body() {
return event[_kRaw].getBodyStream();
},
},
event.context,
);
event[_kRaw].responseCode = res.status;
event[_kRaw].responseMessage = res.statusText;
for (const [key, value] of res.headers) {

const hasSetCookie = res.setCookie?.length > 0;
for (const [key, value] of Object.entries(res.headers)) {
if (key === "set-cookie" && hasSetCookie) {
continue;
}
event[_kRaw].setResponseHeader(key, value);
}
if (res.setCookie?.length > 0) {
for (const cookie of res.setCookie) {
event[_kRaw].appendResponseHeader("set-cookie", cookie);
}
}

return res.body;
});
}
Expand Down
3 changes: 1 addition & 2 deletions src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ export class EventWrapper implements H3Event {
}

get headers(): Headers {
const _headers = this[_kRaw].getHeaders();
return _headers instanceof Headers ? _headers : new Headers(_headers);
return this[_kRaw].getHeaders();
}

toString() {
Expand Down
29 changes: 12 additions & 17 deletions src/types/_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ export interface RawEvent {
readonly remoteAddress?: string | undefined;
readonly isSecure?: boolean | undefined;

getHeader: (key: string) => string | null | undefined;
getHeaders: () => HeadersInit;
getHeader: (key: string) => string | undefined;
getHeaders: () => Headers;

readRawBody: () => MaybePromise<Uint8Array | undefined>;
readTextBody: () => MaybePromise<string | undefined>;
readFormDataBody: () => MaybePromise<FormData | undefined>;
readBodyStream: () => ReadableStream<Uint8Array> | undefined;
getBodyStream: () => ReadableStream<Uint8Array> | undefined;

// -- Response --

Expand All @@ -62,18 +62,13 @@ export interface RawEvent {
responseCode: number | undefined;
responseMessage: string | undefined;

setResponseHeader(key: string, value: string): void;
appendResponseHeader(key: string, value: string): void;
getResponseHeader(key: string): string | null | undefined;
getResponseHeaders(): HeadersInit;
getResponseSetCookie(): string[];
removeResponseHeader(key: string): void;

writeHead(code: number, message?: string): void;

sendResponse(data?: RawResponse): void;

writeEarlyHints(
hints: Record<string, string | string[]>,
): void | Promise<void>;
setResponseHeader: (key: string, value: string) => void;
appendResponseHeader: (key: string, value: string) => void;
getResponseHeader: (key: string) => string | undefined;
getResponseHeaders: () => Headers;
getResponseSetCookie: () => string[];
removeResponseHeader: (key: string) => void;
writeHead: (code: number, message?: string) => void;
sendResponse: (data?: RawResponse) => void;
writeEarlyHints: (hints: Record<string, string>) => void | Promise<void>;
}
Loading

0 comments on commit 8685170

Please sign in to comment.