import {Request} from "./Request"; import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "@extollo/util" import {ServerResponse} from "http" /** * Error thrown when the server tries to re-send headers after they have been sent once. */ export class HeadersAlreadySentError extends ErrorWithContext { constructor(response: Response, headerName?: string) { super(`Cannot modify or re-send headers for this request as they have already been sent.`); this.context = { headerName } } } /** * Error thrown when the server tries to re-send a response that has already been sent. */ export class ResponseAlreadySentError extends ErrorWithContext { constructor(response: Response) { super(`Cannot modify or re-send response as it has already ended.`); } } /** * A class representing an HTTP response to a client. */ export class Response { /** Mapping of headers that should be sent back to the client. */ private headers: {[key: string]: string | string[]} = {} /** True if the headers have been sent. */ private _sentHeaders: boolean = false /** True if the response has been sent and closed. */ private _responseEnded: boolean = false /** The HTTP status code that should be sent to the client. */ private _status: HTTPStatus = HTTPStatus.OK /** * If this is true, then some module in the kernel has flagged the response * as being interrupted and handled. Subsequent modules should NOT overwrite * the response. * @private */ private _blockingWriteback: boolean = false /** The body contents that should be written to the response. */ public body: string = '' /** * Behavior subject fired right before the response content is written. */ public readonly sending$: BehaviorSubject = new BehaviorSubject() /** * Behavior subject fired right after the response content is written. */ public readonly sent$: BehaviorSubject = new BehaviorSubject() constructor( /** The associated request object. */ public readonly request: Request, /** The native Node.js ServerResponse. */ protected readonly serverResponse: ServerResponse, ) { } /** Get the currently set response status. */ public getStatus() { return this._status } /** Set a new response status. */ public setStatus(status: HTTPStatus) { if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status') this._status = status } /** Get the HTTPCookieJar for the client. */ public get cookies() { return this.request.cookies } /** Get the value of the response header, if it exists. */ public getHeader(name: string): string | string[] | undefined { return this.headers[name] } /** Set the value of the response header. */ public setHeader(name: string, value: string | string[]) { if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name) this.headers[name] = value return this } /** * Bulk set the specified headers in the response. * @param data */ public setHeaders(data: {[name: string]: string | string[]}) { if ( this._sentHeaders ) throw new HeadersAlreadySentError(this) this.headers = {...this.headers, ...data} return this } /** * Add the given value as a header, appending it to an existing header if one exists. * @param name * @param value */ public appendHeader(name: string, value: string | string[]) { if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name) if ( !Array.isArray(value) ) value = [value] let existing = this.headers[name] ?? [] if ( !Array.isArray(existing) ) existing = [existing] existing = [...existing, ...value] if ( existing.length === 1 ) existing = existing[0] this.headers[name] = existing } /** * Write the headers to the client. */ public sendHeaders() { const headers = {} as any const setCookieHeaders = this.cookies.getSetCookieHeaders() if ( setCookieHeaders.length ) headers['Set-Cookie'] = setCookieHeaders for ( const key in this.headers ) { if ( !this.headers.hasOwnProperty(key) ) continue headers[key] = this.headers[key] } this.serverResponse.writeHead(this._status, headers) this._sentHeaders = true } /** Returns true if the headers have been sent. */ public hasSentHeaders() { return this._sentHeaders } /** Returns true if a body has been set in the response. */ public hasBody() { return !!this.body } /** * Get or set the flag for whether the writeback should be blocked. * @param set - if this is specified, the value will be set. */ public blockingWriteback(set?: boolean) { if ( typeof set !== 'undefined' ) { this._blockingWriteback = set } return this._blockingWriteback } /** * Write the headers and specified data to the client. * @param data */ public async write(data: any) { return new Promise((res, rej) => { if ( !this._sentHeaders ) this.sendHeaders() this.serverResponse.write(data, error => { if ( error ) rej(error) else res() }) }) } /** * Send the response to the client, writing the headers and configured body. */ public async send() { await this.sending$.next(this) this.setHeader('Content-Length', String(this.body?.length ?? 0)) await this.write(this.body ?? '') this.end() await this.sent$.next(this) } /** * Mark the response as ended and close the socket. */ public end() { if ( this._responseEnded ) throw new ResponseAlreadySentError(this) this._sentHeaders = true this.serverResponse.end() } // location? }