Skip to content

Commit

Permalink
feat: add AsyncHttpContext
Browse files Browse the repository at this point in the history
  • Loading branch information
targos committed May 28, 2021
1 parent 3ac1567 commit 23f883b
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 25 deletions.
30 changes: 30 additions & 0 deletions adonis-typings/async-http-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @adonisjs/http-server
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* 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
}
1 change: 1 addition & 0 deletions adonis-typings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* file that was distributed with this source code.
*/

/// <reference path="./async-http-context.ts" />
/// <reference path="./container.ts" />
/// <reference path="./context.ts" />
/// <reference path="./http-server.ts" />
Expand Down
10 changes: 10 additions & 0 deletions providers/HttpServerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down Expand Up @@ -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
Expand All @@ -98,6 +107,7 @@ export default class HttpServerProvider {
this.registerMiddlewareStore()
this.registerHttpServer()
this.registerHTTPContext()
this.registerAsyncHttpContext()
this.registerRouter()
}
}
49 changes: 49 additions & 0 deletions src/AsyncHttpContext/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @adonisjs/http-server
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/// <reference path="../../adonis-typings/index.ts" />

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<InternalAsyncHttpContext>()

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()
64 changes: 39 additions & 25 deletions src/Server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
})
}
}

0 comments on commit 23f883b

Please sign in to comment.