Skip to content

Commit

Permalink
feat: add support for Deno native HTTP server
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk committed Apr 13, 2021
1 parent be80283 commit 59e7a00
Show file tree
Hide file tree
Showing 31 changed files with 766 additions and 183 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/oak-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: download deno
uses: denoland/setup-deno@main
with:
deno-version: "1.8.3"
deno-version: canary

- name: check format
if: matrix.os == 'ubuntu-latest'
Expand Down
75 changes: 67 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ Also, check out our [FAQs](https://oakserver.github.io/oak/FAQ) and the
[awesome-oak](https://oakserver.github.io/awesome-oak/) site of community
resources.

_Warning_ The examples in this README pull from `main`, which may not make sense
to do when you are looking to actually deploy a workload. You would want to
"pin" to a particular version which is compatible with the version of Deno you
are using and has a fixed set of APIs you would expect. `https://deno.land/x/`
supports using git tags in the URL to direct you at a particular version. So to
use version 3.0.0 of oak, you would want to import
`https://deno.land/x/oak@v3.0.0/mod.ts`.
> ⚠️ _Warning_ The examples in this README pull from `main`, which may not make
> sense to do when you are looking to actually deploy a workload. You would want
> to "pin" to a particular version which is compatible with the version of Deno
> you are using and has a fixed set of APIs you would expect.
> `https://deno.land/x/` supports using git tags in the URL to direct you at a
> particular version. So to use version 3.0.0 of oak, you would want to import
> `https://deno.land/x/oak@v3.0.0/mod.ts`.
## Application, middleware, and context

Expand Down Expand Up @@ -502,6 +502,23 @@ await listenPromise;
// and you can do something after the close to shutdown
```

### Deno `std/http` versus native bindings

As of Deno 1.9, Deno has a _native_ HTTP server. oak automatically detects if
these APIs are available and will listen and process requests using the native
server. Currently the APIs are marked as _unstable_ so in order to use them, you
need to start your server with the `--unstable` flag. For example:

```
> deno run --allow-net --unstable server.ts
```

Currently there are two features that are not yet supported with the native
server, those are:

- Server Sent Events
- Upgrading a request to a WebSocket

### Just handling requests

In situations where you don't want the application to listen for requests on the
Expand All @@ -510,7 +527,49 @@ can use the `.handle()` method. For example if you are in a serverless function
where the requests are arriving in a different way.

The `.handle()` method will invoke the middleware, just like the middleware gets
invoked for each request that is processed by `.listen()`. It take up to two
invoked for each request that is processed by `.listen()`. The method works with
either `std/http` requests/responses, or it handles the _native_ Deno responses.

#### Handling _native_ requests and responses

When using the native requests, the `.handle()` method accepts up to three
arguments. The first being a
[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) argument,
and the second being a `Deno.Conn` argument. The third optional argument is a
flag to indicate if the request was "secure" in the sense it originated from a
TLS connection to the remote client. The method resolved with a
[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
or `undefined` if the `ctx.respond === true`.

An example:

```ts
import { Application } from "https://deno.land/x/oak/mod.ts";

const app = new Application();

app.use((ctx) => {
ctx.response.body = "Hello World!";
});

const listener = Deno.listen({ hostname: "localhost", port: 8000 });

for await (const conn of listener) {
(async () => {
const requests = Deno.serveHttp(conn);
for await (const { request, respondWith } of requests) {
const response = await app.handle(request, conn);
if (response) {
respondWith(response);
}
}
});
}
```

#### Handling `std/http` requests and responses

When using `std/http` requests the `.handle()` method accepts up to two
arguments, one being the request which conforms to the `std/http/server`'s
`ServerRequest` interface and an optional second argument which is if the
request is "secure" in the sense it originated from a TLS connection to the
Expand Down
135 changes: 81 additions & 54 deletions application.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
// Copyright 2018-2021 the oak authors. All rights reserved. MIT license.

import { Context } from "./context.ts";
import { assert, Status, STATUS_TEXT } from "./deps.ts";
import {
serve as defaultServe,
serveTLS as defaultServeTls,
Status,
STATUS_TEXT,
} from "./deps.ts";
hasNativeHttp,
HttpServerNative,
NativeRequest,
} from "./http_server_native.ts";
import { HttpServerStd } from "./http_server_std.ts";
import type { ServerRequest, ServerResponse } from "./http_server_std.ts";
import { Key, KeyStack } from "./keyStack.ts";
import { compose, Middleware } from "./middleware.ts";
import type {
Serve,
Server,
ServerRequest,
ServerResponse,
ServeTls,
} from "./types.d.ts";

export interface ListenOptionsBase {
hostname?: string;
port: number;
import { Server, ServerConstructor } from "./types.d.ts";
import { isConn } from "./util.ts";

export interface ListenOptionsBase extends Deno.ListenOptions {
secure?: false;
signal?: AbortSignal;
}

export interface ListenOptionsTls {
certFile: string;
hostname?: string;
keyFile: string;
port: number;
export interface ListenOptionsTls extends Deno.ListenTlsOptions {
secure: true;
signal?: AbortSignal;
}

export type ListenOptions = ListenOptionsTls | ListenOptionsBase;

function isOptionsTls(options: ListenOptions): options is ListenOptionsTls {
return options.secure === true;
export interface HandleMethod {
(
request: ServerRequest,
secure?: boolean,
): Promise<ServerResponse | undefined>;
(
request: Request,
conn: Deno.Conn,
secure?: boolean,
): Promise<Response | undefined>;
}

export type ListenOptions = ListenOptionsTls | ListenOptionsBase;

interface ApplicationErrorEventListener<S> {
(evt: ApplicationErrorEvent<S>): void | Promise<void>;
}
Expand Down Expand Up @@ -67,6 +66,7 @@ interface ApplicationListenEventInit extends EventInit {
hostname?: string;
port: number;
secure: boolean;
serverType: "std" | "native" | "custom";
}

type ApplicationListenEventListenerOrEventListenerObject =
Expand All @@ -82,15 +82,12 @@ export interface ApplicationOptions<S> {
* This defaults to `false`. */
proxy?: boolean;

/** The `server()` function to be used to read requests.
*
* _Not used generally, as this is just for mocking for test purposes_ */
serve?: Serve;

/** The `server()` function to be used to read requests.
/** A server constructor to use instead of the default server for receiving
* requests.
*
* _Not used generally, as this is just for mocking for test purposes_ */
serveTls?: ServeTls;
* _This is not generally used, except for mocking and testing._
*/
serverConstructor?: ServerConstructor<ServerRequest | NativeRequest>;

/** The initial state object for the application, of which the type can be
* used to infer the type of the state for both the application and any of the
Expand All @@ -102,7 +99,7 @@ interface RequestState {
handling: Set<Promise<void>>;
closing: boolean;
closed: boolean;
server: Server;
server: Server<ServerRequest | NativeRequest>;
}

// deno-lint-ignore no-explicit-any
Expand All @@ -123,12 +120,14 @@ export class ApplicationListenEvent extends Event {
hostname?: string;
port: number;
secure: boolean;
serverType: "std" | "native" | "custom";

constructor(eventInitDict: ApplicationListenEventInit) {
super("listen", eventInitDict);
this.hostname = eventInitDict.hostname;
this.port = eventInitDict.port;
this.secure = eventInitDict.secure;
this.serverType = eventInitDict.serverType;
}
}

Expand All @@ -144,8 +143,7 @@ export class Application<AS extends State = Record<string, any>>
#composedMiddleware?: (context: Context<AS>) => Promise<void>;
#keys?: KeyStack;
#middleware: Middleware<State, Context<State>>[] = [];
#serve: Serve;
#serveTls: ServeTls;
#serverConstructor: ServerConstructor<ServerRequest | NativeRequest>;

/** A set of keys, or an instance of `KeyStack` which will be used to sign
* cookies read and set by the application to avoid tampering with the
Expand Down Expand Up @@ -190,15 +188,13 @@ export class Application<AS extends State = Record<string, any>>
state,
keys,
proxy,
serve = defaultServe,
serveTls = defaultServeTls,
serverConstructor = hasNativeHttp() ? HttpServerNative : HttpServerStd,
} = options;

this.proxy = proxy ?? false;
this.keys = keys;
this.state = state ?? {} as AS;
this.#serve = serve;
this.#serveTls = serveTls;
this.#serverConstructor = serverConstructor;
}

#getComposed = (): ((context: Context<AS>) => Promise<void>) => {
Expand Down Expand Up @@ -242,7 +238,7 @@ export class Application<AS extends State = Record<string, any>>

/** Processing registered middleware on each request. */
#handleRequest = async (
request: ServerRequest,
request: ServerRequest | NativeRequest,
secure: boolean,
state: RequestState,
): Promise<void> => {
Expand All @@ -263,16 +259,22 @@ export class Application<AS extends State = Record<string, any>>
state.handling.delete(handlingPromise);
return;
}
let closeResources = true;
try {
await request.respond(await context.response.toServerResponse());
if (request instanceof NativeRequest) {
closeResources = false;
await request.respond(await context.response.toDomResponse());
} else {
await request.respond(await context.response.toServerResponse());
}
if (state.closing) {
state.server.close();
state.closed = true;
}
} catch (err) {
this.#handleError(context, err);
} finally {
context.response.destroy();
context.response.destroy(closeResources);
resolve!();
state.handling.delete(handlingPromise);
}
Expand Down Expand Up @@ -309,14 +311,32 @@ export class Application<AS extends State = Record<string, any>>
* context gets set to not to respond, then the method resolves with
* `undefined`, otherwise it resolves with a request that is compatible with
* `std/http/server`. */
handle = async (
request: ServerRequest,
handle = (async (
request: ServerRequest | Request,
secureOrConn: Deno.Conn | boolean | undefined,
secure = false,
): Promise<ServerResponse | undefined> => {
): Promise<ServerResponse | Response | undefined> => {
if (!this.#middleware.length) {
throw new TypeError("There is no middleware to process requests.");
}
const context = new Context(this, request, secure);
let contextRequest: ServerRequest | NativeRequest;
if (request instanceof Request) {
assert(isConn(secureOrConn));
contextRequest = new NativeRequest({
request,
respondWith() {
return Promise.resolve(undefined);
},
}, secureOrConn);
} else {
assert(
typeof secureOrConn === "boolean" ||
typeof secureOrConn === "undefined",
);
secure = secureOrConn ?? false;
contextRequest = request;
}
const context = new Context(this, contextRequest, secure);
try {
await this.#getComposed()(context);
} catch (err) {
Expand All @@ -327,14 +347,18 @@ export class Application<AS extends State = Record<string, any>>
return;
}
try {
const response = await context.response.toServerResponse();
context.response.destroy();
const response = contextRequest instanceof NativeRequest
? await context.response.toDomResponse()
: await context.response.toServerResponse();
context.response.destroy(false);
return response;
} catch (err) {
// deno-lint-ignore no-unreachable
this.#handleError(context, err);
// deno-lint-ignore no-unreachable
throw err;
}
};
}) as HandleMethod;

/** Start listening for requests, processing registered middleware on each
* request. If the options `.secure` is undefined or `false`, the listening
Expand All @@ -360,9 +384,7 @@ export class Application<AS extends State = Record<string, any>>
const [, hostname, portStr] = match;
options = { hostname, port: parseInt(portStr, 10) };
}
const server = isOptionsTls(options)
? this.#serveTls(options)
: this.#serve(options);
const server = new this.#serverConstructor(options);
const { signal } = options;
const state = {
closed: false,
Expand All @@ -380,8 +402,13 @@ export class Application<AS extends State = Record<string, any>>
});
}
const { hostname, port, secure = false } = options;
const serverType = server instanceof HttpServerStd
? "std"
: server instanceof HttpServerNative
? "native"
: "custom";
this.dispatchEvent(
new ApplicationListenEvent({ hostname, port, secure }),
new ApplicationListenEvent({ hostname, port, secure, serverType }),
);
try {
for await (const request of server) {
Expand Down
Loading

0 comments on commit 59e7a00

Please sign in to comment.