From 25712c2a5607f4a4424603cb3e70bbe8729328ab Mon Sep 17 00:00:00 2001 From: George Karagkiaouris Date: Wed, 8 Dec 2021 13:26:58 -0500 Subject: [PATCH] Initial abstract classes --- packages/next/server/api-utils.ts | 2 +- packages/next/server/base-http.ts | 273 ++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 packages/next/server/base-http.ts diff --git a/packages/next/server/api-utils.ts b/packages/next/server/api-utils.ts index d44c6fc8db9ed..fcd975c3967f7 100644 --- a/packages/next/server/api-utils.ts +++ b/packages/next/server/api-utils.ts @@ -141,7 +141,7 @@ export async function apiResolver( * @param req request object */ export async function parseBody( - req: NextApiRequest, + req: IncomingMessage, limit: string | number ): Promise { let contentType diff --git a/packages/next/server/base-http.ts b/packages/next/server/base-http.ts new file mode 100644 index 0000000000000..e1780edac9128 --- /dev/null +++ b/packages/next/server/base-http.ts @@ -0,0 +1,273 @@ +import type { ServerResponse, IncomingMessage } from 'http' +import type { Writable, Readable } from 'stream' +import { parseNextUrl } from '../dist/shared/lib/router/utils/parse-next-url' +import { PERMANENT_REDIRECT_STATUS } from '../shared/lib/constants' +import { ParsedNextUrl } from '../shared/lib/router/utils/parse-next-url' +import { parseBody } from './api-utils' +import { I18NConfig } from './config-shared' + +export interface BaseNextRequestConfig { + basePath: string | undefined + i18n?: I18NConfig + trailingSlash?: boolean | undefined +} + +interface RequestMeta { + // interface from `server/request-meta.ts` +} + +export abstract class BaseNextRequest { + constructor( + public url: string, + public method: string, + public body: Body, + public config: BaseNextRequestConfig + ) {} + + abstract parseBody(limit: string | number): Promise + + abstract getHeader(name: string): string | string[] | undefined + + abstract getAllHeaders(): Record + + // Utils implemented using the abstract methods above + + public nextUrl: ParsedNextUrl = parseNextUrl({ + headers: this.getAllHeaders(), + nextConfig: this.config, + url: this.url, + }) + + public meta: RequestMeta = {} +} + +class NodeNextRequest extends BaseNextRequest { + constructor(public req: IncomingMessage, config: BaseNextRequestConfig) { + super(req.url!, req.method!.toUpperCase(), req, config) + } + + async parseBody(limit: string | number): Promise { + return parseBody(this.req, limit) + } + + getHeader(name: string): string | string[] | undefined { + return this.req.headers[name] + } + + getAllHeaders(): Record { + const result: Record = {} + + for (const [name, value] of Object.entries(this.req.headers)) { + if (value !== undefined) { + result[name] = value + } + } + + return result + } +} + +class WebNextRequest extends BaseNextRequest { + constructor(public request: Request, config: BaseNextRequestConfig) { + super( + request.url, + request.method.toUpperCase(), + request.clone().body, + config + ) + } + + async parseBody(_limit: string | number): Promise { + // TODO: implement parseBody for web + return + } + + getHeader(name: string): string | undefined { + return this.request.headers.get(name) ?? undefined + } + + getAllHeaders(): Record { + const result: Record = {} + + for (const [name, value] of this.request.headers.entries()) { + result[name] = value + } + + return result + } +} + +export abstract class BaseNextResponse { + abstract statusCode: number | undefined + abstract statusMessage: string | undefined + abstract get sent(): boolean + + constructor(public destination: Destination) {} + + /** + * Sets a value for the header overwriting existing values + */ + abstract setHeader(name: string, value: string): this + + /** + * Appends value for the given header name + */ + abstract appendHeader(name: string, value: string): this + + /** + * Get all vaues for a header as an array or undefined if no value is present + */ + abstract getHeaderValues(name: string): string[] | undefined + + /** + * Get vaues for a header concatenated using `,` or undefined if no value is present + */ + abstract getHeader(name: string): string | undefined + + abstract body(value: string): this + + abstract send(): void + + // Utils implemented using the abstract methods above + + redirect(destination: string, statusCode: number) { + this.setHeader('Location', destination) + this.statusCode = statusCode + + if (statusCode === PERMANENT_REDIRECT_STATUS) { + this.setHeader('Refresh', `0;url=${destination}`) + } + return this + } +} + +class NodeNextResponse extends BaseNextResponse { + private textBody: string | undefined = undefined + + constructor(public res: ServerResponse) { + super(res) + } + + get sent() { + return this.res.finished || this.res.headersSent + } + + get statusCode() { + return this.res.statusCode + } + + set statusCode(value: number) { + this.res.statusCode = value + } + + get statusMessage() { + return this.res.statusMessage + } + + set statusMessage(value: string) { + this.res.statusMessage = value + } + + setHeader(name: string, value: string): this { + this.res.setHeader(name, value) + return this + } + + getHeaderValues(name: string): string[] | undefined { + const values = this.res.getHeader(name) + + if (values === undefined) return undefined + + return (Array.isArray(values) ? values : [values]).map((value) => + value.toString() + ) + } + + getHeader(name: string): string | undefined { + const values = this.getHeaderValues(name) + return Array.isArray(values) ? values.join(',') : undefined + } + + appendHeader(name: string, value: string): this { + const currentValues = this.getHeaderValues(name) ?? [] + + if (!currentValues.includes(value)) { + this.res.setHeader(name, [...currentValues, value]) + } + + return this + } + + body(value: string) { + this.textBody = value + return this + } + + send() { + this.res.end(this.textBody) + } +} + +class WebNextResponse extends BaseNextResponse { + private headers = new Headers() + private textBody: string | undefined = undefined + private _sent = false + + private sendPromise = new Promise((resolve) => { + this.sendResolve = resolve + }) + private sendResolve?: () => void + private response = this.sendPromise.then(() => { + return new Response(this.textBody ?? this.transformStream.readable, { + headers: this.headers, + status: this.statusCode, + statusText: this.statusMessage, + }) + }) + + public statusCode: number | undefined + public statusMessage: string | undefined + + get sent() { + return this._sent + } + + constructor(public transformStream = new TransformStream()) { + super(transformStream.writable) + } + + setHeader(name: string, value: string): this { + this.headers.set(name, value) + return this + } + + getHeaderValues(name: string): string[] | undefined { + // https://developer.mozilla.org/en-US/docs/Web/API/Headers/get#example + return this.getHeader(name) + ?.split(',') + .map((v) => v.trimStart()) + } + + getHeader(name: string): string | undefined { + return this.headers.get(name) ?? undefined + } + + appendHeader(name: string, value: string): this { + this.headers.append(name, value) + return this + } + + body(value: string) { + this.textBody = value + return this + } + + send() { + this.sendResolve?.() + this._sent = true + } + + toResponse() { + return this.response + } +}