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

Base Http for BaseServer #32999

Merged
merged 16 commits into from Jan 14, 2022
Merged
Expand Up @@ -25,6 +25,7 @@ import cookie from 'next/dist/compiled/cookie'
import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants'
import { NextConfig } from '../../../../server/config'
import { addRequestMeta } from '../../../../server/request-meta'
import { BaseNextRequest } from '../../../../server/base-http'

const getCustomRouteMatcher = pathMatch(true)

Expand Down Expand Up @@ -85,7 +86,10 @@ export function getUtils({
defaultRouteMatches = dynamicRouteMatcher(page) as ParsedUrlQuery
}

function handleRewrites(req: IncomingMessage, parsedUrl: UrlWithParsedQuery) {
function handleRewrites(
req: BaseNextRequest | IncomingMessage,
parsedUrl: UrlWithParsedQuery
) {
for (const rewrite of rewrites) {
const matcher = getCustomRouteMatcher(rewrite.source)
let params = matcher(parsedUrl.pathname)
Expand Down Expand Up @@ -158,7 +162,7 @@ export function getUtils({
}

function getParamsFromRouteMatches(
req: IncomingMessage,
req: BaseNextRequest | IncomingMessage,
renderOpts?: any,
detectedLocale?: string
) {
Expand Down Expand Up @@ -269,7 +273,10 @@ export function getUtils({
return pathname
}

function normalizeVercelUrl(req: IncomingMessage, trustQuery: boolean) {
function normalizeVercelUrl(
req: BaseNextRequest | IncomingMessage,
trustQuery: boolean
) {
// make sure to normalize req.url on Vercel to strip dynamic params
// from the query which are added during routing
if (pageIsDynamic && trustQuery && defaultRouteRegex) {
Expand Down Expand Up @@ -374,7 +381,7 @@ export function getUtils({
if (detectedDomain) {
defaultLocale = detectedDomain.defaultLocale
detectedLocale = defaultLocale
addRequestMeta(req, '__nextIsLocaleDomain', true)
addRequestMeta(req as any, '__nextIsLocaleDomain', true)
}

// if not domain specific locale use accept-language preferred
Expand All @@ -394,7 +401,7 @@ export function getUtils({
...parsedUrl,
pathname: localePathResult.pathname,
})
addRequestMeta(req, '__nextStrippedLocale', true)
addRequestMeta(req as any, '__nextStrippedLocale', true)
parsedUrl.pathname = localePathResult.pathname
}

Expand Down
7 changes: 4 additions & 3 deletions packages/next/server/api-utils.ts
Expand Up @@ -10,6 +10,7 @@ import { sendEtagResponse } from './send-payload'
import generateETag from 'next/dist/compiled/etag'
import isError from '../lib/is-error'
import { interopDefault } from '../lib/interop-default'
import { BaseNextRequest, BaseNextResponse } from './base-http'

export type NextApiRequestCookies = { [key: string]: string }
export type NextApiRequestQuery = { [key: string]: string | string[] }
Expand Down Expand Up @@ -141,7 +142,7 @@ export async function apiResolver(
* @param req request object
*/
export async function parseBody(
req: NextApiRequest,
req: IncomingMessage,
limit: string | number
): Promise<any> {
let contentType
Expand Down Expand Up @@ -338,8 +339,8 @@ export const SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA)
const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS)

export function tryGetPreviewData(
req: IncomingMessage,
res: ServerResponse,
req: IncomingMessage | BaseNextRequest,
res: ServerResponse | BaseNextResponse,
options: __ApiPreviewProps
): PreviewData {
// Read cached preview data if present
Expand Down
264 changes: 264 additions & 0 deletions packages/next/server/base-http.ts
@@ -0,0 +1,264 @@
import type { ServerResponse, IncomingMessage, IncomingHttpHeaders } from 'http'
import type { Writable, Readable } from 'stream'
import { PERMANENT_REDIRECT_STATUS } from '../shared/lib/constants'
// import { ParsedNextUrl, parseNextUrl } from '../shared/lib/router/utils/parse-next-url'
import { getCookieParser, NextApiRequestCookies, parseBody } from './api-utils'
import { I18NConfig } from './config-shared'
import { NEXT_REQUEST_META, RequestMeta } from './request-meta'

export interface BaseNextRequestConfig {
basePath: string | undefined
i18n?: I18NConfig
trailingSlash?: boolean | undefined
}

export abstract class BaseNextRequest<Body = any> {
protected _cookies: NextApiRequestCookies | undefined
public abstract headers: IncomingHttpHeaders

constructor(public method: string, public url: string, public body: Body) {}

abstract parseBody(limit: string | number): Promise<any>

// Utils implemented using the abstract methods above

public get cookies() {
if (this._cookies) return this._cookies

return (this._cookies = getCookieParser(this.headers)())
}
}

export class NodeNextRequest extends BaseNextRequest<Readable> {
public headers = this.req.headers

set [NEXT_REQUEST_META](value: RequestMeta) {
// Mirror meta object to Node request for when `getRequestMeta` gets called on it
// This still happens in render.tsx
this.req[NEXT_REQUEST_META] = value
}

constructor(
public req: IncomingMessage & { [NEXT_REQUEST_META]?: RequestMeta }
) {
super(req.method!.toUpperCase(), req.url!, req)
}

async parseBody(limit: string | number): Promise<any> {
return parseBody(this.req, limit)
}
}

export class WebNextRequest extends BaseNextRequest<ReadableStream | null> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Not blocking) maybe we can separate Web and Node request/response in 2 files just like we did for base-server and web-server.

public request: Request
public headers: IncomingHttpHeaders

constructor(request: Request) {
const url = new URL(request.url)

super(
request.method,
url.href.slice(url.origin.length),
request.clone().body
)
this.request = request

this.headers = {}
for (const [name, value] of request.headers.entries()) {
this.headers[name] = value
}
}

async parseBody(_limit: string | number): Promise<any> {
// TODO: implement parseBody for web
shuding marked this conversation as resolved.
Show resolved Hide resolved
return
}
}

export abstract class BaseNextResponse<Destination = any> {
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 | 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

abstract hasHeader(name: string): boolean

/**
* 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
}
}

export class NodeNextResponse extends BaseNextResponse<Writable> {
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 | 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()
)
}

hasHeader(name: string): boolean {
return this.res.hasHeader(name)
}

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)
}
}

export class WebNextResponse extends BaseNextResponse<WritableStream> {
private headers = new Headers()
private textBody: string | undefined = undefined
private _sent = false

private sendPromise = new Promise<void>((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 | string[]): this {
this.headers.delete(name)
for (const val of Array.isArray(value) ? value : [value]) {
this.headers.append(name, val)
}
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
}

hasHeader(name: string): boolean {
return this.headers.has(name)
}

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
}
}