From 23f883b4cb19180e2b5bce4b62eb42f15657e96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Fri, 28 May 2021 10:17:40 +0200 Subject: [PATCH] feat: add AsyncHttpContext --- adonis-typings/async-http-context.ts | 30 +++++++++++++ adonis-typings/index.ts | 1 + providers/HttpServerProvider.ts | 10 +++++ src/AsyncHttpContext/index.ts | 49 +++++++++++++++++++++ src/Server/index.ts | 64 +++++++++++++++++----------- 5 files changed, 129 insertions(+), 25 deletions(-) create mode 100644 adonis-typings/async-http-context.ts create mode 100644 src/AsyncHttpContext/index.ts diff --git a/adonis-typings/async-http-context.ts b/adonis-typings/async-http-context.ts new file mode 100644 index 0000000..7ff48aa --- /dev/null +++ b/adonis-typings/async-http-context.ts @@ -0,0 +1,30 @@ +/** + * @adonisjs/http-server + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare module '@ioc:Adonis/Core/AsyncHttpContext' { + import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + + /** + * Async Http context available during the lifecycle of HTTP requests + */ + export interface AsyncHttpContextContract { + /** + * Returns the current HTTP context or null if called outside of a request. + */ + getContext(): HttpContextContract | null + + /** + * Returns the current HTTP context or throws if called outside of a request. + */ + getContextOrFail(): HttpContextContract + } + + const AsyncHttpContext: AsyncHttpContextContract + export default AsyncHttpContext +} diff --git a/adonis-typings/index.ts b/adonis-typings/index.ts index 44c7753..da5f3ee 100644 --- a/adonis-typings/index.ts +++ b/adonis-typings/index.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +/// /// /// /// diff --git a/providers/HttpServerProvider.ts b/providers/HttpServerProvider.ts index e9df306..49b2dcd 100644 --- a/providers/HttpServerProvider.ts +++ b/providers/HttpServerProvider.ts @@ -10,6 +10,8 @@ import { Exception } from '@poppinss/utils' import { ApplicationContract } from '@ioc:Adonis/Core/Application' +import { asyncHttpContext } from '../src/AsyncHttpContext' + export default class HttpServerProvider { constructor(protected application: ApplicationContract) {} @@ -80,6 +82,13 @@ export default class HttpServerProvider { }) } + /** + * Register the async HTTP context + */ + protected registerAsyncHttpContext() { + this.application.container.singleton('Adonis/Core/AsyncHttpContext', () => asyncHttpContext) + } + /** * Register the router. The router points to the instance of router used * by the middleware @@ -98,6 +107,7 @@ export default class HttpServerProvider { this.registerMiddlewareStore() this.registerHttpServer() this.registerHTTPContext() + this.registerAsyncHttpContext() this.registerRouter() } } diff --git a/src/AsyncHttpContext/index.ts b/src/AsyncHttpContext/index.ts new file mode 100644 index 0000000..6be8cea --- /dev/null +++ b/src/AsyncHttpContext/index.ts @@ -0,0 +1,49 @@ +/** + * @adonisjs/http-server + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// + +import { AsyncLocalStorage } from 'async_hooks' +import { Exception } from '@poppinss/utils' +import { AsyncHttpContextContract } from '@ioc:Adonis/Core/AsyncHttpContext' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +const adonisLocalStorage = new AsyncLocalStorage() + +export class InternalAsyncHttpContext { + constructor(private ctx: HttpContextContract) {} + + public getContext() { + return this.ctx + } + + public run(callback: () => any) { + return adonisLocalStorage.run(this, callback) + } +} + +class AsyncHttpContext implements AsyncHttpContextContract { + public getContext() { + const store = adonisLocalStorage.getStore() + if (store) { + return store.getContext() + } + return null + } + + public getContextOrFail() { + const store = adonisLocalStorage.getStore() + if (store) { + return store.getContext() + } + throw new Exception('AsyncHttpContext accessed outside of a request context') + } +} + +export const asyncHttpContext = new AsyncHttpContext() diff --git a/src/Server/index.ts b/src/Server/index.ts index 0600be4..e71db80 100644 --- a/src/Server/index.ts +++ b/src/Server/index.ts @@ -27,6 +27,7 @@ import { HttpContext } from '../HttpContext' import { RequestHandler } from './RequestHandler' import { MiddlewareStore } from '../MiddlewareStore' import { ExceptionManager } from './ExceptionManager' +import { InternalAsyncHttpContext } from '../AsyncHttpContext' /** * Server class handles the HTTP requests by using all Adonis micro modules. @@ -123,6 +124,13 @@ export class Server implements ServerContract { ) } + /** + * Returns a new async HTTP context for the new request + */ + private getAsyncContext(ctx: HttpContextContract): InternalAsyncHttpContext { + return new InternalAsyncHttpContext(ctx) + } + /** * Define custom error handler to handler all errors * occurred during HTTP request @@ -160,34 +168,40 @@ export class Server implements ServerContract { const requestAction = this.getProfilerRow(request) const ctx = this.getContext(request, response, requestAction) + const asyncContext = this.getAsyncContext(ctx) /* - * Handle request by executing hooks, request middleware stack - * and route handler + * Run everything within the async HTTP context */ - try { - await this.handleRequest(ctx) - } catch (error) { - await this.exception.handle(error, ctx) - } + return asyncContext.run(async () => { + /* + * Handle request by executing hooks, request middleware stack + * and route handler + */ + try { + await this.handleRequest(ctx) + } catch (error) { + await this.exception.handle(error, ctx) + } - /* - * Excute hooks when there are one or more hooks. The `ctx.response.finish` - * is intentionally inside both the `try` and `catch` blocks as a defensive - * measure. - * - * When we call `response.finish`, it will serialize the response body and may - * encouter errors while doing so and hence will be catched by the catch - * block. - */ - try { - await this.hooks.executeAfter(ctx) - requestAction.end({ status_code: res.statusCode }) - ctx.response.finish() - } catch (error) { - await this.exception.handle(error, ctx) - requestAction.end({ status_code: res.statusCode, error }) - ctx.response.finish() - } + /* + * Excute hooks when there are one or more hooks. The `ctx.response.finish` + * is intentionally inside both the `try` and `catch` blocks as a defensive + * measure. + * + * When we call `response.finish`, it will serialize the response body and may + * encouter errors while doing so and hence will be catched by the catch + * block. + */ + try { + await this.hooks.executeAfter(ctx) + requestAction.end({ status_code: res.statusCode }) + ctx.response.finish() + } catch (error) { + await this.exception.handle(error, ctx) + requestAction.end({ status_code: res.statusCode, error }) + ctx.response.finish() + } + }) } }