docs: document Deno's HTTP Server API (#10280)

Co-authored-by: Satya Rohith <me@satyarohith.com>
This commit is contained in:
Kitson Kelly 2021-04-22 06:57:02 +10:00 committed by GitHub
parent 3b78f6c449
commit 21372d7b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 318 additions and 11 deletions

View File

@ -2,25 +2,75 @@
## Concepts
- Use the std library [http module](https://deno.land/std@$STD_VERSION/http) to
run your own web server.
- Use Deno's integrated HTTP server to run your own web server.
## Overview
With just a few lines of code you can run your own http web server with control
With just a few lines of code you can run your own HTTP web server with control
over the response status, request headers and more.
> The _native_ HTTP server is currently unstable, meaning the API is not
> finalized and may change in breaking ways in future version of Deno. To have
> the APIs discussed here available, you must run Deno with the `--unstable`
> flag.
## Sample web server
In this example, the user-agent of the client is returned to the client:
```typescript
/**
* webserver.ts
*/
**webserver.ts**:
```ts
// Start listening on port 8080 of localhost.
const server = Deno.listen({ port: 8080 });
console.log(`HTTP webserver running. Access it at: http://localhost:8080/`);
// Connections to the server will be yielded up as an async iterable.
for await (const conn of server) {
// In order to not be blocking, we need to handle each connection individually
// in its own async function.
(async () => {
// This "upgrades" a network connection into an HTTP connection.
const httpConn = Deno.serveHttp(conn);
// Each request sent over the HTTP connection will be yielded as an async
// iterator from the HTTP connection.
for await (const requestEvent of httpConn) {
// The native HTTP server uses the web standard `Request` and `Response`
// objects.
const body = `Your user-agent is:\n\n${requestEvent.request.headers.get(
"user-agent",
) ?? "Unknown"}`;
// The requestEvent's `.respondWith()` method is how we send the response
// back to the client.
requestEvent.respondWith(
new Response(body, {
status: 200,
}),
);
}
})();
}
```
Then run this with:
```shell
deno run --allow-net --unstable webserver.ts
```
Then navigate to `http://localhost:8080/` in a browser.
### Using the `std/http` library
If you do not want to use the unstable APIs, you can still use the standard
library's HTTP server:
**webserver.ts**:
```ts
import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";
const server = serve({ hostname: "0.0.0.0", port: 8080 });
const server = serve({ port: 8080 });
console.log(`HTTP webserver running. Access it at: http://localhost:8080/`);
for await (const request of server) {
@ -31,7 +81,7 @@ for await (const request of server) {
}
```
Run this with:
Then run this with:
```shell
deno run --allow-net webserver.ts

View File

@ -15,8 +15,8 @@ For more details, view the chapter on
## `Deno` global
All APIs that are not web standard are contained in the global `Deno` namespace.
It has the APIs for reading from files, opening TCP sockets, and executing
subprocesses, etc.
It has the APIs for reading from files, opening TCP sockets, serving HTTP, and
executing subprocesses, etc.
The TypeScript definitions for the Deno namespaces can be found in the
[`lib.deno.ns.d.ts`](https://github.com/denoland/deno/blob/$CLI_VERSION/cli/dts/lib.deno.ns.d.ts)

View File

@ -0,0 +1,256 @@
## HTTP Server APIs
As of Deno 1.9 and later, _native_ HTTP server APIs were introduced which allow
users to create robust and performant web servers in Deno.
The API tries to leverage as much of the web standards as is possible as well as
tries to be simple and straight forward.
> The APIs are currently unstable, meaning they can change in the future in
> breaking ways and should be carefully considered before using in production
> code. They require the `--unstable` flag to make them available.
### Listening for a connection
In order to accept requests, first you need to listen for a connection on a
network port. To do this in Deno, you use `Deno.listen()`:
```ts
const server = Deno.listen({ port: 8080 });
```
> When supplying a port, Deno assumes you are going to listen on a TCP socket
> as well as bind to the localhost. You can specify `transport: "tcp"` to be
> more explicit as well as provide an IP address or hostname in the `hostname`
> property as well.
If there is an issue with opening the network port, `Deno.listen()` will throw,
so often in a server sense, you will want to wrap it in the `try ... catch`
block in order to handle exceptions, like the port already being in use.
You can also listen for a TLS connection (e.g. HTTPS) using `Deno.listenTls()`:
```ts
const server = Deno.listenTls({
port: 8443,
certFile: "localhost.crt",
keyFile: "localhost.key",
alpnProtocols: ["h2", "http/1.1"],
});
```
The `certFile` and `keyFile` options are required and point to the appropriate
certificate and key files for the server. They are relative to the CWD for Deno.
The `alpnProtocols` property is optional, but if you want to be able to support
HTTP/2 on the server, you add the protocols here, as the protocol negotiation
happens during the TLS negotiation with the client and server.
> Generating SSL certificates is outside of the scope of this documentation.
> There are many resources on the web which address this.
### Handling connections
Once we are listening for a connection, we need to handle the connection. The
return value of `Deno.listen()` or `Deno.listenTls()` is a `Deno.Listener` which
is an async iterable which yields up `Deno.Conn` connections as well as provide
a couple methods for handling connections.
To use it as an async iterable we would do something like this:
```ts
const server = Deno.listen({ port: 8080 });
for await (const conn of server) {
// ...handle the connection...
}
```
Every connection made would yielded up a `Deno.Conn` assigned to `conn`. Then
further processing can be applied to the connection.
There is also the `.accept()` method on the listener which can be used:
```ts
const server = Deno.listen({ port: 8080 });
while (true) {
const conn = server.accept();
if (conn) {
// ... handle the connection ...
} else {
// The listener has closed
break;
}
}
```
Whether using the async iterator or the `.accept()` method, exceptions can be
thrown and robust production code should handle these using `try ... catch`
blocks. Especially when it comes to accepting TLS connections, there can be many
conditions, like invalid or unknown certificates which can be surfaced on the
listener and might need handling in the user code.
A listener also has a `.close()` method which can be used to close the listener.
### Serving HTTP
Once a connection is accepted, you can use `Deno.serveHttp()` to handle HTTP
requests and responses on the connection. `Deno.serveHttp()` returns a
`Deno.HttpConn`. A `Deno.HttpConn` is like a `Deno.Listener` in that requests
the connection receives from the client are asynchronously yielded up as a
`Deno.RequestEvent`.
To deal with HTTP requests as async iterable it would look something like this:
```ts
const server = Deno.listen({ port: 8080 });
for await (const conn of server) {
(async () => {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
// ... handle requestEvent ...
}
})();
}
```
The `Deno.HttpConn` also has the method `.nextRequest()` which can be used to
await the next request. It would look something like this:
```ts
const server = Deno.listen({ port: 8080 });
while (true) {
const conn = server.accept();
if (conn) {
(async () => {
const httpConn = Deno.serveHttp(conn);
while (true) {
const requestEvent = await httpConn.nextRequest();
if (requestEvent) {
// ... handle requestEvent ...
} else {
// the connection has finished
break;
}
}
})();
} else {
// The listener has closed
break;
}
}
```
Note that in both cases we are using an IIFE to create an inner function to deal
with each connection. If we awaited the HTTP requests in the same function scope
as the one we were receiving the connections, we would be blocking accepting
additional connections, which would make it seem that our server was "frozen".
In practice, it might make more sense to have a separate function all together:
```ts
async function handle(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
// ... handle requestEvent
}
}
const server = Deno.listen({ port: 8080 });
for await (const conn of server) {
handle(conn);
}
```
In the examples from this point on, we will focus on what would occur within an
example `handle()` function and remove the listening and connection
"boilerplate".
### HTTP Requests and Responses
HTTP requests and responses in Deno are essentially the inverse of web standard
[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). The
Deno HTTP Server API and the Fetch API leverage the
[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and
[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
classes. So if you are familiar with the Fetch API you just need to flip them
around in your mind and now it is a server API.
As mentioned above, a `Deno.HttpConn` asynchronously yields up
`Deno.RequestEvent`s. These request events contain a `.request` property and a
`.respondWith()` method.
The `.request` property is an instance of the `Request` class with the
information about the request. For example, if we wanted to know what URL path
was being requested, we would do something like this:
```ts
async function handle(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
const url = new URL(requestEvent.request.url);
console.log(`path: ${url.path}`);
}
}
```
The `.respondWith()` method is how we complete a request. The method takes
either a `Response` object or a `Promise` which resolves with a `Response`
object. Responding with a basic "hello world" would look like this:
```ts
async function handle(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
await requestEvent.respondWith(new Response("hello world"), {
status: 200,
});
}
}
```
Note that we awaited the `.respondWith()` method. It isn't required, but in
practice any errors in processing the response will cause the promise returned
from the method to be rejected, like if the client disconnected before all the
response could be sent. While there may not be anything your application needs
to do, not handling the rejection will cause an "unhandled rejection" to occur
which will terminate the Deno process, which isn't so good for a server. In
addition, you might want to await the promise returned in order to determine
when to do any cleanup from for the request/response cycle.
The web standard `Response` object is pretty powerful, allowing easy creation of
complex and rich responses to a client, and Deno strives to provide a `Response`
object that as closely matches the web standard as possible, so if you are
wondering how to send a particular response, checkout out the documentation for
the web standard
[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
### HTTP/2 Support
HTTP/2 support is effectively transparent within the Deno runtime. Typically
HTTP/2 is negotiated between a client and a server during the TLS connection
setup via
[ALPN](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation). To
enable this, you need to provide the protocols you want to support when you
start listening via the `alpnProtocols` property. This will enable the
negotiation to occur when the connection is made. For example:
```ts
const server = Deno.listenTls({
port: 8443,
certFile: "localhost.crt",
keyFile: "localhost.key",
alpnProtocols: ["h2", "http/1.1"],
});
```
The protocols are provided in order of preference. In practice, the only two
protocols that are supported currently are HTTP/2 and HTTP/1.1 which are
expressed as `h2` and `http/1.1`.
Currently Deno does not support upgrading a plain-text HTTP/1.1 connection to an
HTTP/2 cleartext connection via the `Upgrade` header (see:
[#10275](https://github.com/denoland/deno/issues/10275)), so therefore HTTP/2
support is only available via a TLS/HTTPS connection.

View File

@ -21,6 +21,7 @@
"program_lifecycle": "Program lifecycle",
"permission_apis": "Permission APIs",
"web_platform_apis": "Web Platform APIs",
"http_server_apis": "HTTP Server APIs",
"location_api": "Location API",
"workers": "Workers"
}