diff --git a/src/http/HTTPError.ts b/src/http/HTTPError.ts index 77a4afe..e7af9f8 100644 --- a/src/http/HTTPError.ts +++ b/src/http/HTTPError.ts @@ -5,7 +5,7 @@ export class HTTPError extends ErrorWithContext { public readonly status: HTTPStatus = 500, public readonly message: string = '' ) { - super(message || HTTPMessage[status]) + super('HTTP ERROR') this.message = message || HTTPMessage[status] } } diff --git a/src/http/kernel/HTTPKernel.ts b/src/http/kernel/HTTPKernel.ts index 66c93ff..f302c30 100644 --- a/src/http/kernel/HTTPKernel.ts +++ b/src/http/kernel/HTTPKernel.ts @@ -1,9 +1,10 @@ -import {Instantiable, Singleton, Inject} from "@extollo/di" -import {Collection} from "@extollo/util" +import {Inject, Instantiable, Singleton} from "@extollo/di" +import {Collection, HTTPStatus} from "@extollo/util" import {HTTPKernelModule} from "./HTTPKernelModule"; import {Logging} from "../../service/Logging"; import {AppClass} from "../../lifecycle/AppClass"; import {Request} from "../lifecycle/Request"; +import {http} from "../response/HTTPErrorResponseFactory"; /** * Interface for fluently registering kernel modules into the kernel. @@ -72,11 +73,10 @@ export class HTTPKernel extends AppClass { } } catch (e: any) { this.logging.error(e) - // FIXME handle error response - // const error_response = error(e) - // await error_response.write(request) + await http(HTTPStatus.REQUEST_TIMEOUT).write(request) } + this.logging.verbose('Finished kernel lifecycle') return request } diff --git a/src/http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule.ts b/src/http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule.ts new file mode 100644 index 0000000..866f6d7 --- /dev/null +++ b/src/http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule.ts @@ -0,0 +1,39 @@ +import {HTTPKernelModule} from "../HTTPKernelModule"; +import {HTTPKernel} from "../HTTPKernel"; +import {Request} from "../../lifecycle/Request"; +import {ActivatedRoute} from "../../routing/ActivatedRoute"; +import {ResponseObject} from "../../routing/Route"; +import {plaintext} from "../../response/StringResponseFactory"; +import {ResponseFactory} from "../../response/ResponseFactory"; +import {json} from "../../response/JSONResponseFactory"; +import {http} from "../../response/HTTPErrorResponseFactory"; +import {HTTPStatus} from "@extollo/util"; + +export class ExecuteResolvedRouteHandlerHTTPModule extends HTTPKernelModule { + public static register(kernel: HTTPKernel) { + kernel.register(this).core() + } + + public async apply(request: Request) { + if ( request.hasInstance(ActivatedRoute) ) { + const route = request.make(ActivatedRoute) + let object: ResponseObject = await route.handler(request) + + if ( (typeof object === 'string') || (typeof object === 'number') ) { + object = plaintext(String(object)) + } + + if ( object instanceof ResponseFactory ) { + await object.write(request) + } else if ( typeof object !== 'undefined' ) { + await json(object).write(request) + } else { + await plaintext('').write(request) + } + } else { + await http(HTTPStatus.NOT_FOUND).write(request) + } + + return request + } +} diff --git a/src/http/lifecycle/Response.ts b/src/http/lifecycle/Response.ts index 44c992b..e575506 100644 --- a/src/http/lifecycle/Response.ts +++ b/src/http/lifecycle/Response.ts @@ -1,5 +1,5 @@ import {Request} from "./Request"; -import {ErrorWithContext, HTTPStatus} from "@extollo/util" +import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "@extollo/util" import {ServerResponse} from "http" export class HeadersAlreadySentError extends ErrorWithContext { @@ -20,7 +20,9 @@ export class Response { private _sentHeaders: boolean = false private _responseEnded: boolean = false private _status: HTTPStatus = HTTPStatus.OK - public body: any + public body: string = '' + public readonly sending$: BehaviorSubject = new BehaviorSubject() + public readonly sent$: BehaviorSubject = new BehaviorSubject() constructor( public readonly request: Request, @@ -96,9 +98,12 @@ export class Response { }) } - public async send(data?: any) { - await this.write(data ?? this.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) } public end() { diff --git a/src/http/response/ResponseFactory.ts b/src/http/response/ResponseFactory.ts index 5f44988..7d10b07 100644 --- a/src/http/response/ResponseFactory.ts +++ b/src/http/response/ResponseFactory.ts @@ -1,5 +1,4 @@ import {HTTPStatus} from "@extollo/util" -import {Instantiable} from "@extollo/di" import {Request} from "../lifecycle/Request" export abstract class ResponseFactory { diff --git a/src/http/session/MemorySession.ts b/src/http/session/MemorySession.ts index c28cae1..63dbb17 100644 --- a/src/http/session/MemorySession.ts +++ b/src/http/session/MemorySession.ts @@ -20,6 +20,8 @@ export class MemorySession extends Session { protected sessionID?: string protected data?: SessionData + constructor() { super() } + public getKey(): string { if ( !this.sessionID ) throw new NoSessionKeyError() return this.sessionID diff --git a/src/http/session/SessionFactory.ts b/src/http/session/SessionFactory.ts index cdbd1d6..009280e 100644 --- a/src/http/session/SessionFactory.ts +++ b/src/http/session/SessionFactory.ts @@ -17,6 +17,8 @@ export class SessionFactory extends AbstractFactory { protected readonly logging: Logging protected readonly config: Config + private static loggedMemorySessionWarningOnce = false + constructor() { super({}) this.logging = Container.getContainer().make(Logging) @@ -24,7 +26,6 @@ export class SessionFactory extends AbstractFactory { } produce(dependencies: any[], parameters: any[]): Session { - this.logging.warn(`You are using the default memory-based session driver. It is recommended you configure a persistent session driver instead.`) return new (this.getSessionClass()) } @@ -53,8 +54,11 @@ export class SessionFactory extends AbstractFactory { protected getSessionClass() { const SessionClass = this.config.get('server.session.driver', MemorySession) + if ( SessionClass === MemorySession && !SessionFactory.loggedMemorySessionWarningOnce ) { + this.logging.warn(`You are using the default memory-based session driver. It is recommended you configure a persistent session driver instead.`) + SessionFactory.loggedMemorySessionWarningOnce = true + } - // TODO check that session class is valid if ( !isInstantiable(SessionClass) || !(SessionClass.prototype instanceof Session) ) { const e = new ErrorWithContext('Provided session class does not extend from @extollo/lib.Session'); e.context = { diff --git a/src/index.ts b/src/index.ts index 0a5e251..e091b8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ export * from './lifecycle/Application' export * from './lifecycle/AppClass' export * from './lifecycle/Unit' +export * from './http/HTTPError' + export * from './http/kernel/module/InjectSessionHTTPModule' export * from './http/kernel/module/MountActivatedRouteHTTPModule' export * from './http/kernel/module/PersistSessionHTTPModule' @@ -32,12 +34,13 @@ export * from './http/routing/ActivatedRoute' export * from './http/routing/Route' export * from './http/routing/RouteGroup' +export * from './http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule' + export * from './http/session/Session' export * from './http/session/SessionFactory' export * from './http/session/MemorySession' export * from './http/Controller' -export * from './http/HTTPError' export * from './service/Canonical' export * from './service/CanonicalInstantiable' diff --git a/src/service/HTTPServer.ts b/src/service/HTTPServer.ts index 3741061..62d6124 100644 --- a/src/service/HTTPServer.ts +++ b/src/service/HTTPServer.ts @@ -1,6 +1,7 @@ -import {Singleton, Inject} from "@extollo/di" +import {Inject, Singleton} from "@extollo/di" +import {HTTPStatus, withTimeout} from "@extollo/util" import {Unit} from "../lifecycle/Unit"; -import {createServer, IncomingMessage, ServerResponse, Server} from "http"; +import {createServer, IncomingMessage, Server, ServerResponse} from "http"; import {Logging} from "./Logging"; import {Request} from "../http/lifecycle/Request"; import {HTTPKernel} from "../http/kernel/HTTPKernel"; @@ -9,6 +10,7 @@ import {SetSessionCookieHTTPModule} from "../http/kernel/module/SetSessionCookie import {InjectSessionHTTPModule} from "../http/kernel/module/InjectSessionHTTPModule"; import {PersistSessionHTTPModule} from "../http/kernel/module/PersistSessionHTTPModule"; import {MountActivatedRouteHTTPModule} from "../http/kernel/module/MountActivatedRouteHTTPModule"; +import {ExecuteResolvedRouteHandlerHTTPModule} from "../http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule"; @Singleton() export class HTTPServer extends Unit { @@ -29,6 +31,7 @@ export class HTTPServer extends Unit { InjectSessionHTTPModule.register(this.kernel) PersistSessionHTTPModule.register(this.kernel) MountActivatedRouteHTTPModule.register(this.kernel) + ExecuteResolvedRouteHandlerHTTPModule.register(this.kernel) await new Promise((res, rej) => { this.server = createServer(this.handler) @@ -55,8 +58,26 @@ export class HTTPServer extends Unit { public get handler() { return async (request: IncomingMessage, response: ServerResponse) => { const extolloReq = new Request(request, response) + + // FIXME make timeout configurable + withTimeout(10000, extolloReq.response.sent$.toPromise()) + .onTime(req => { + this.logging.verbose(`Request lifecycle finished on time. (Path: ${extolloReq.path})`) + }) + .late(req => { + this.logging.warn(`Request lifecycle finished late, so an error response was returned! (Path: ${extolloReq.path})`) + }) + .timeout(() => { + this.logging.error(`Request lifecycle has timed out. Will send error response instead. (Path: ${extolloReq.path})`) + extolloReq.response.setStatus(HTTPStatus.REQUEST_TIMEOUT) + extolloReq.response.body = 'Sorry, your request timed out.' + extolloReq.response.send() + }) + .run() + .catch(e => this.logging.error(e)) + await this.kernel.handle(extolloReq) - await extolloReq.response.send('Hi, from Extollo!!') + await extolloReq.response.send() } } } diff --git a/src/tsconfig.json b/src/tsconfig.json index eab8150..7f677c5 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "experimentalDecorators": true, "module": "commonjs", - "target": "es5", + "target": "es6", "sourceMap": true }, "exclude": [ diff --git a/tsconfig.json b/tsconfig.json index f764f26..637b913 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "module": "commonjs", "declaration": true, "outDir": "./lib",