import { Request } from './Request'
import { ErrorWithContext , HTTPStatus , BehaviorSubject } from '../../util'
import { ServerResponse } from 'http'
import { HTTPCookieJar } from '../kernel/HTTPCookieJar'
import { Readable } from 'stream'
import { Logging } from '../../service/Logging'
/ * *
* 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 ( public readonly 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 = false
/** True if the response has been sent and closed. */
private responseEnded = 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 module s should NOT overwrite
* the response .
* @private
* /
private isBlockingWriteback = false
/** The body contents that should be written to the response. */
public body : string | Buffer | Uint8Array | Readable = ''
/ * *
* 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 ,
) { }
protected get logging ( ) : Logging {
return this . request . make ( Logging )
}
/** Get the currently set response status. */
public getStatus ( ) : HTTPStatus {
return this . status
}
/** Set a new response status. */
public setStatus ( status : HTTPStatus ) : this {
if ( this . sentHeaders ) {
throw new HeadersAlreadySentError ( this , 'status' )
}
this . status = status
return this
}
/** 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 [ ] ) : this {
this . logging . verbose ( ` Will set header on response: ${ name } ` )
if ( this . sentHeaders ) {
throw new HeadersAlreadySentError ( this , name )
}
this . headers [ name ] = value
return this
}
/ * *
* Remove a header from the response by name .
* @param name
* /
public unsetHeader ( name : string ) : this {
this . logging . verbose ( ` Will unset header on response: ${ name } ` )
if ( this . sentHeaders ) {
throw new HeadersAlreadySentError ( this , name )
}
delete this . headers [ name ]
return this
}
/ * *
* Bulk set the specified headers in the response .
* @param data
* /
public setHeaders ( data : { [ name : string ] : string | string [ ] } ) : this {
this . logging . verbose ( ` Will set headers on response: ${ Object . keys ( data ) . join ( ', ' ) } ` )
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 [ ] ) : this {
this . logging . verbose ( ` Will append header: ${ name } ` )
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
return this
}
/ * *
* Write the headers to the client .
* /
public sendHeaders ( ) : this {
this . logging . verbose ( ` Sending headers... ` )
if ( ! this . serverResponse ) {
throw new ErrorWithContext ( 'Unable to send headers: Response has no underlying connection.' , {
suggestion : 'This usually means the Request was created by an alternative server, like WebsocketServer. You should use that server to handle the request.' ,
} )
}
const headers = { } as any
const setCookieHeaders = this . cookies . getSetCookieHeaders ( )
if ( setCookieHeaders . length ) {
headers [ 'Set-Cookie' ] = setCookieHeaders
}
for ( const key in this . headers ) {
if ( ! Object . prototype . hasOwnProperty . call ( this . headers , key ) ) {
continue
}
headers [ key ] = this . headers [ key ]
}
this . serverResponse . writeHead ( this . status , headers )
this . sentHeaders = true
return this
}
/** Returns true if the headers have been sent. */
public hasSentHeaders ( ) : boolean {
return this . sentHeaders
}
/** Returns true if a body has been set in the response. */
public hasBody ( ) : boolean {
return Boolean ( 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 ) : boolean {
if ( typeof set !== 'undefined' ) {
this . isBlockingWriteback = set
}
return this . isBlockingWriteback
}
/ * *
* Write the headers and specified data to the client .
* @param data
* /
public async write ( data : string | Buffer | Uint8Array | Readable ) : Promise < void > {
this . logging . verbose ( ` Writing headers & data to response... (destroyed? ${ ! this . serverResponse || this . serverResponse . destroyed } ) ` )
return new Promise < void > ( ( res , rej ) = > {
if ( ! this . serverResponse ) {
throw new ErrorWithContext ( 'Unable to write response: Response has no underlying connection.' , {
suggestion : 'This usually means the Request was created by an alternative server, like WebsocketServer. You should use that server to handle the request.' ,
} )
}
if ( this . responseEnded || this . serverResponse . destroyed ) {
throw new ErrorWithContext ( 'Tried to write to Response after lifecycle ended.' )
}
if ( ! this . sentHeaders ) {
this . sendHeaders ( )
}
if ( data instanceof Readable ) {
data . pipe ( this . serverResponse )
. on ( 'finish' , ( ) = > {
res ( )
} )
. on ( 'error' , error = > {
rej ( error )
} )
} else {
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 ( ) : Promise < void > {
await this . sending $ . next ( this )
if ( ! ( this . body instanceof Readable ) ) {
this . setHeader ( 'Content-Length' , String ( Buffer . from ( this . body ? ? '' ) . length ) )
}
this . setHeader ( 'Date' , ( new Date ( ) ) . toUTCString ( ) )
this . setHeader ( 'Permissions-Policy' , 'interest-cohort=()' )
await this . write ( this . body ? ? '' )
this . end ( )
await this . sent $ . next ( this )
}
/ * *
* Returns true if the response can still be sent . False if it has been sent
* or the connection has been destroyed .
* /
public canSend ( ) : boolean {
return ! ( this . responseEnded || ! this . serverResponse || this . serverResponse . destroyed )
}
/ * *
* Mark the response as ended and close the socket .
* /
public end ( ) : this {
if ( this . responseEnded ) {
throw new ResponseAlreadySentError ( this )
}
this . sentHeaders = true
this . serverResponse ? . end ( )
return this
}
// location?
}