You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/http/lifecycle/Response.ts

200 lines
6.1 KiB

import {Request} from "./Request";
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "../../util"
import {ServerResponse} from "http"
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
/**
* 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<Response> = new BehaviorSubject<Response>()
/**
* Behavior subject fired right after the response content is written.
*/
public readonly sent$: BehaviorSubject<Response> = new BehaviorSubject<Response>()
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(): HTTPCookieJar {
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<void>((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?
}