From 0b4940a6d6f717bb6fa9e199495eb12d6065d475 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sat, 28 Aug 2021 10:45:48 +0200 Subject: [PATCH 01/84] :construction: --- http/middleware.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 http/middleware.ts diff --git a/http/middleware.ts b/http/middleware.ts new file mode 100644 index 000000000000..727ce8b88fcb --- /dev/null +++ b/http/middleware.ts @@ -0,0 +1,31 @@ +type Handler< + A, + B = A, +> = (req: A, next?: Handler) => void + +function addMiddleware< + A, + B, + C, +>( + stack: Handler, + middleware: Handler, +): Handler { + return (req, next) => stack( + req, + r => middleware(r, next), + ) +} + +const authMiddleware: Handler<{}, { auth: string }> = (req, next) => { + const auth = 'someuser' + const response = next({ auth }) + + return +} +const handleGet: Handler<{}> = (req: {}) => {} + +addMiddleware( + authMiddleware, + handleGet, +) From 55c520e543d2acf71e1dd26520b582651b14a3ce Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 5 Sep 2021 21:47:08 +0200 Subject: [PATCH 02/84] :construction: --- http/middleware.ts | 37 +++++++++++++------------------------ http/playground.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 http/playground.ts diff --git a/http/middleware.ts b/http/middleware.ts index 727ce8b88fcb..61b74e1b932e 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,31 +1,20 @@ -type Handler< - A, - B = A, -> = (req: A, next?: Handler) => void +export type HttpRequest = { path: string } +export type HttpResponse = { body: string } -function addMiddleware< - A, - B, - C, +export type Middleware< + Requires extends HttpRequest, + Adds = {}, +> = (req: Gets, next?: Middleware) => HttpResponse + +export function addMiddleware< + StackAdd, + HandlerAdd, >( - stack: Handler, - middleware: Handler, -): Handler { + stack: Middleware, + middleware: Middleware, +): Middleware { return (req, next) => stack( req, r => middleware(r, next), ) } - -const authMiddleware: Handler<{}, { auth: string }> = (req, next) => { - const auth = 'someuser' - const response = next({ auth }) - - return -} -const handleGet: Handler<{}> = (req: {}) => {} - -addMiddleware( - authMiddleware, - handleGet, -) diff --git a/http/playground.ts b/http/playground.ts new file mode 100644 index 000000000000..3b5634a41992 --- /dev/null +++ b/http/playground.ts @@ -0,0 +1,34 @@ +import { HttpRequest, HttpResponse, Middleware, addMiddleware } from "./middleware.ts" + +type AuthedRequest = HttpRequest & { auth: string } + +const authenticate: Middleware = (req, next) => { + const auth = req.path + const response = next!({ + ...req, + auth, + }) + + return response +} + +const authorize = (req: R, next?: Middleware) => { + const isAuthorized = req.auth === 'admin' + + if (!isAuthorized) { + return { body: 'nope' } + } + + return next!(req) +} + + +const passThrough: Middleware = (req, next) => next!(req) + +const handleGet: Middleware = req => ({ body: "lawl" }) + +const stack = passThrough +const withAuthentication = addMiddleware(stack, authenticate) +const withAuthorization = addMiddleware(withAuthentication, authorize) +const handler = addMiddleware(withAuthorization, handleGet) + From c862fef02adf3cc18d7047c84bc8a32a26045f96 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 6 Sep 2021 00:06:31 +0200 Subject: [PATCH 03/84] :construction: --- http/playground.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/http/playground.ts b/http/playground.ts index 3b5634a41992..c3620b4ecc45 100644 --- a/http/playground.ts +++ b/http/playground.ts @@ -1,7 +1,10 @@ -import { HttpRequest, HttpResponse, Middleware, addMiddleware } from "./middleware.ts" +import { assertEquals } from '../testing/asserts.ts' +import { HttpRequest, Middleware, addMiddleware } from './middleware.ts' type AuthedRequest = HttpRequest & { auth: string } +const passThrough: Middleware = (req, next) => next!(req) + const authenticate: Middleware = (req, next) => { const auth = req.path const response = next!({ @@ -22,13 +25,15 @@ const authorize = (req: R, next?: Middleware) => { return next!(req) } +const rainbow: Middleware = (req, next) => next!({ ...req, rainbow: true }) -const passThrough: Middleware = (req, next) => next!(req) - -const handleGet: Middleware = req => ({ body: "lawl" }) +const handleGet: Middleware = req => ({ body: 'yey' }) const stack = passThrough const withAuthentication = addMiddleware(stack, authenticate) const withAuthorization = addMiddleware(withAuthentication, authorize) -const handler = addMiddleware(withAuthorization, handleGet) +const withRainbow = addMiddleware(withAuthorization, rainbow) +const withHandler = addMiddleware(withRainbow, handleGet) +assertEquals(withHandler({ path: 'someuser' }), { body: 'nope' }) +assertEquals(withHandler({ path: 'admin' }), { body: 'yey' }) From 096c99db62052195dd3710decef265d7b904384b Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Fri, 10 Sep 2021 00:09:23 +0200 Subject: [PATCH 04/84] :rotating_light: Format and fix lint problems --- http/middleware.ts | 61 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index 61b74e1b932e..1db09a56dc60 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,20 +1,53 @@ -export type HttpRequest = { path: string } -export type HttpResponse = { body: string } +export type HttpRequest = { path: string }; +export type HttpResponse = { body: string }; export type Middleware< - Requires extends HttpRequest, - Adds = {}, -> = (req: Gets, next?: Middleware) => HttpResponse + Requires extends HttpRequest, + // deno-lint-ignore ban-types + Adds = {}, +> = ( + req: Gets, + next?: Middleware, +) => Promise; -export function addMiddleware< - StackAdd, - HandlerAdd, +type MiddlewareStack< + Requires extends HttpRequest, + // deno-lint-ignore ban-types + Adds = {}, +> = { + handler: Middleware; + + add( + middleware: Middleware, + ): MiddlewareStack; +}; + +function addMiddleware< + StackAdd, + HandlerAdd, >( - stack: Middleware, - middleware: Middleware, + stack: Middleware, + middleware: Middleware, ): Middleware { - return (req, next) => stack( - req, - r => middleware(r, next), - ) + return (req, next) => + stack( + req, + (r) => middleware(r, next), + ); +} + +// deno-lint-ignore ban-types +export function stack( + middleware: Middleware, +): MiddlewareStack { + return { + handler: middleware, + add: (m) => + stack( + addMiddleware( + middleware, + m, + ), + ), + }; } From 994126caccb760c58196a35ef20773f8b7f5c348 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 12 Sep 2021 15:42:35 +0200 Subject: [PATCH 05/84] :construction: --- http/middleware.ts | 19 ++--- http/middleware/json.ts | 27 +++++++ http/middleware/poc/server.ts | 39 ++++++++++ http/middleware/poc/validate_zoo_zonfig.ts | 35 +++++++++ http/middleware/poc/zoo.ts | 17 +++++ http/middleware/yaml.ts | 29 +++++++ http/playground.ts | 88 ++++++++++++---------- http/router.ts | 15 ++++ 8 files changed, 219 insertions(+), 50 deletions(-) create mode 100644 http/middleware/json.ts create mode 100644 http/middleware/poc/server.ts create mode 100644 http/middleware/poc/validate_zoo_zonfig.ts create mode 100644 http/middleware/poc/zoo.ts create mode 100644 http/middleware/yaml.ts create mode 100644 http/router.ts diff --git a/http/middleware.ts b/http/middleware.ts index 1db09a56dc60..8137c15221a4 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,8 +1,5 @@ -export type HttpRequest = { path: string }; -export type HttpResponse = { body: string }; - export type Middleware< - Requires extends HttpRequest, + Requires extends Request, // deno-lint-ignore ban-types Adds = {}, > = ( @@ -11,24 +8,24 @@ export type Middleware< ) => Promise; type MiddlewareStack< - Requires extends HttpRequest, + Requires extends Request, // deno-lint-ignore ban-types Adds = {}, > = { handler: Middleware; add( - middleware: Middleware, - ): MiddlewareStack; + middleware: Middleware, + ): MiddlewareStack; }; function addMiddleware< StackAdd, HandlerAdd, >( - stack: Middleware, - middleware: Middleware, -): Middleware { + stack: Middleware, + middleware: Middleware, +): Middleware { return (req, next) => stack( req, @@ -37,7 +34,7 @@ function addMiddleware< } // deno-lint-ignore ban-types -export function stack( +export function stack( middleware: Middleware, ): MiddlewareStack { return { diff --git a/http/middleware/json.ts b/http/middleware/json.ts new file mode 100644 index 000000000000..1c9f92878b11 --- /dev/null +++ b/http/middleware/json.ts @@ -0,0 +1,27 @@ +import { Middleware } from '../middleware.ts' + +export const acceptJson: Middleware = async (req, next) => { + const body = await req.text() + + let parsedBody: unknown + + try { + parsedBody = JSON.parse(body) + } catch(e) { + if (e instanceof SyntaxError) { + return new Response( + e.message, + { status: 422, statusText: 'Request could not be parsed' } + ) + } + + throw e + } + + const nextReq = { + ...req, + parsedBody, + } + + return next!(nextReq) +} diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts new file mode 100644 index 000000000000..16ed34cb988d --- /dev/null +++ b/http/middleware/poc/server.ts @@ -0,0 +1,39 @@ +import { router } from "../../router.ts"; +import { listenAndServe } from "../../server.ts"; +import { acceptYaml } from "../yaml.ts"; +import { acceptJson } from "../json.ts"; +import { stack } from "../../middleware.ts"; +import { validateZoo } from "./validate_zoo_zonfig.ts"; +import { Zoo } from "./zoo.ts"; + +async function createZoo(req: Request & { zoo: Zoo }) { + const { zoo } = req; + const responseMessage = + `Your nice ${zoo.name} Zoo was created. Take good care of your ${zoo.animals.length} animals!`; + + return new Response(responseMessage, { + status: 201, + statusText: "Zoo created", + }); +} + +const handleCreateZoo = stack(validateZoo) + .add(createZoo) + .handler; + +const handler = router(new Map([ + [ + [ /^\/zoos\/yaml$/, 'POST' ], + stack(acceptYaml) + .add(handleCreateZoo) + .handler, + ], + [ + [ /^\/zoos\/json/, 'POST' ], + stack(acceptJson) + .add(handleCreateZoo) + .handler, + ], +])) + +await listenAndServe('', handler) diff --git a/http/middleware/poc/validate_zoo_zonfig.ts b/http/middleware/poc/validate_zoo_zonfig.ts new file mode 100644 index 000000000000..d0c39f5e322d --- /dev/null +++ b/http/middleware/poc/validate_zoo_zonfig.ts @@ -0,0 +1,35 @@ +import { Middleware } from '../../middleware.ts' +import { includesValue } from '../../../collections/includes_value.ts' +import { AnimalKind, Zoo } from './zoo.ts' + +function isZoo(subject: unknown): subject is Zoo { + const cast = subject as Zoo + + return typeof cast === 'object' + && typeof cast.name ==='string' + && typeof cast.entryFee === 'number' + && Array.isArray(cast.animals) + && (cast.animals as unknown[]).every(isAnimalKind) +} + +function isAnimalKind(subject: unknown): subject is AnimalKind { + return typeof subject === 'string' && includesValue(AnimalKind, subject) +} + +export const validateZoo: Middleware = async (req, next) => { + const { parsedBody } = req + + if (!isZoo(parsedBody)) { + return new Response(null, { + status: 422, + statusText: 'Invalid ZooConfig', + }) + } + + const nextReq = { + ...req, + zoo: parsedBody, + } + + return next!(nextReq) +} diff --git a/http/middleware/poc/zoo.ts b/http/middleware/poc/zoo.ts new file mode 100644 index 000000000000..1556f18f2445 --- /dev/null +++ b/http/middleware/poc/zoo.ts @@ -0,0 +1,17 @@ +export enum AnimalKind { + Tiger = 'Tiger', + Elephant = 'Elephant', + RedPanda = 'Red Panda', + Monkey = 'Monkey', + Hippo = 'Hippo', +} + +export type Zoo = { + name: string + entryFee: number + animals: { + name: string + kind: AnimalKind + }[] +} + diff --git a/http/middleware/yaml.ts b/http/middleware/yaml.ts new file mode 100644 index 000000000000..6c9b1b112cb8 --- /dev/null +++ b/http/middleware/yaml.ts @@ -0,0 +1,29 @@ +import { Middleware } from '../middleware.ts' +import { parse } from "../../encoding/yaml.ts" +import { YAMLError } from "../../encoding/_yaml/error.ts" + +export const acceptYaml: Middleware = async (req, next) => { + const body = await req.text() + + let parsedBody: unknown + + try { + parsedBody = parse(body) + } catch(e) { + if (e instanceof YAMLError) { + return new Response( + e.toString(false), + { status: 422, statusText: 'Request could not be parsed' } + ) + } + + throw e + } + + const nextReq = { + ...req, + parsedBody, + } + + return next!(nextReq) +} diff --git a/http/playground.ts b/http/playground.ts index c3620b4ecc45..b63789be1d0b 100644 --- a/http/playground.ts +++ b/http/playground.ts @@ -1,39 +1,49 @@ -import { assertEquals } from '../testing/asserts.ts' -import { HttpRequest, Middleware, addMiddleware } from './middleware.ts' - -type AuthedRequest = HttpRequest & { auth: string } - -const passThrough: Middleware = (req, next) => next!(req) - -const authenticate: Middleware = (req, next) => { - const auth = req.path - const response = next!({ - ...req, - auth, - }) - - return response -} - -const authorize = (req: R, next?: Middleware) => { - const isAuthorized = req.auth === 'admin' - - if (!isAuthorized) { - return { body: 'nope' } - } - - return next!(req) -} - -const rainbow: Middleware = (req, next) => next!({ ...req, rainbow: true }) - -const handleGet: Middleware = req => ({ body: 'yey' }) - -const stack = passThrough -const withAuthentication = addMiddleware(stack, authenticate) -const withAuthorization = addMiddleware(withAuthentication, authorize) -const withRainbow = addMiddleware(withAuthorization, rainbow) -const withHandler = addMiddleware(withRainbow, handleGet) - -assertEquals(withHandler({ path: 'someuser' }), { body: 'nope' }) -assertEquals(withHandler({ path: 'admin' }), { body: 'yey' }) +import { assertEquals } from "../testing/asserts.ts"; +import { HttpRequest, Middleware, stack } from "./middleware.ts"; + +type AuthedRequest = HttpRequest & { auth: string }; + +const passThrough: Middleware = async (req, next) => next!(req); + +const authenticate: Middleware = async ( + req, + next, +) => { + const auth = req.path; + const response = await next!({ + ...req, + auth, + }); + + return response; +}; + +const authorize = async ( + req: R, + next?: Middleware, +) => { + const isAuthorized = req.auth === "admin"; + + if (!isAuthorized) { + return { body: "nope" }; + } + + return next!(req); +}; + +const rainbow: Middleware = async ( + req, + next, +) => next!({ ...req, rainbow: true }); + +const handleGet: Middleware = async (req) => ({ body: "yey" }); + +const combinedHandler = stack(passThrough) + .add(authenticate) + .add(authorize) + .add(rainbow) + .add(handleGet) + .handler; + +assertEquals(await combinedHandler({ path: "someuser" }), { body: "nope" }); +assertEquals(await combinedHandler({ path: "admin" }), { body: "yey" }); diff --git a/http/router.ts b/http/router.ts new file mode 100644 index 000000000000..81fef4d4463d --- /dev/null +++ b/http/router.ts @@ -0,0 +1,15 @@ +import { Handler } from './mod.ts' + +export function router(routes: Map<[RegExp, Request['method']], Handler>): Handler { + return async (req, con) => { + const { pathname: path } = new URL(req.url) + + for (const [ [ pattern, method ], handler ] of routes) { + if (req.method === method && pattern.exec(path) !== null) { + return await handler(req, con) + } + } + + return new Response(null, { status: 404, statusText: 'Route not found' }) + } +} From e1fc75ada7b8c3e16e067e6c92434c577ff850d8 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 12 Sep 2021 21:10:46 +0200 Subject: [PATCH 06/84] :construction: --- http/middleware.ts | 16 ++++--- http/middleware/json.ts | 46 ++++++++++--------- http/middleware/log.ts | 14 ++++++ http/middleware/poc/server.ts | 40 +++++++++++------ http/middleware/poc/validate_zoo_zonfig.ts | 51 ++++++++++++---------- http/middleware/poc/zoo.ts | 25 +++++------ http/middleware/yaml.ts | 50 +++++++++++---------- http/playground.ts | 49 --------------------- http/router.ts | 24 +++++----- 9 files changed, 155 insertions(+), 160 deletions(-) create mode 100644 http/middleware/log.ts delete mode 100644 http/playground.ts diff --git a/http/middleware.ts b/http/middleware.ts index 8137c15221a4..72bd6008b3fe 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,11 +1,14 @@ +import { ConnInfo } from "./server.ts"; + export type Middleware< Requires extends Request, // deno-lint-ignore ban-types Adds = {}, > = ( req: Gets, + con: ConnInfo, next?: Middleware, -) => Promise; +) => Promise; type MiddlewareStack< Requires extends Request, @@ -23,13 +26,14 @@ function addMiddleware< StackAdd, HandlerAdd, >( - stack: Middleware, - middleware: Middleware, + first: Middleware, + second: Middleware, ): Middleware { - return (req, next) => - stack( + return (req, con, next) => + first( req, - (r) => middleware(r, next), + con, + (r) => second(r, con, next), ); } diff --git a/http/middleware/json.ts b/http/middleware/json.ts index 1c9f92878b11..918f1ff20e20 100644 --- a/http/middleware/json.ts +++ b/http/middleware/json.ts @@ -1,27 +1,31 @@ -import { Middleware } from '../middleware.ts' +import { Middleware } from "../middleware.ts"; -export const acceptJson: Middleware = async (req, next) => { - const body = await req.text() +export const acceptJson: Middleware = async ( + req, + con, + next, +) => { + const body = await req.text(); - let parsedBody: unknown + let parsedBody: unknown; - try { - parsedBody = JSON.parse(body) - } catch(e) { - if (e instanceof SyntaxError) { - return new Response( - e.message, - { status: 422, statusText: 'Request could not be parsed' } - ) - } - - throw e + try { + parsedBody = JSON.parse(body); + } catch (e) { + if (e instanceof SyntaxError) { + return new Response( + e.message, + { status: 422, statusText: "Request could not be parsed" }, + ); } - const nextReq = { - ...req, - parsedBody, - } + throw e; + } + + const nextReq = { + ...req, + parsedBody, + }; - return next!(nextReq) -} + return next!(nextReq, con); +}; diff --git a/http/middleware/log.ts b/http/middleware/log.ts new file mode 100644 index 000000000000..1083165f2308 --- /dev/null +++ b/http/middleware/log.ts @@ -0,0 +1,14 @@ +import { Middleware } from "../middleware.ts"; + +export const log: Middleware = async (req, con, next) => { + const start = performance.now(); + const response = await next!(req, con); + const end = performance.now(); + + console.log( + `${req.method} ${new URL(req.url).pathname}\t\t${response.status}\t\t${end - + start}ms`, + ); + + return response; +}; diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index 16ed34cb988d..c4407c0e538e 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -1,15 +1,25 @@ import { router } from "../../router.ts"; +import { distinctBy } from "../../../collections/mod.ts"; import { listenAndServe } from "../../server.ts"; import { acceptYaml } from "../yaml.ts"; import { acceptJson } from "../json.ts"; +import { log } from "../log.ts"; import { stack } from "../../middleware.ts"; import { validateZoo } from "./validate_zoo_zonfig.ts"; import { Zoo } from "./zoo.ts"; async function createZoo(req: Request & { zoo: Zoo }) { const { zoo } = req; - const responseMessage = - `Your nice ${zoo.name} Zoo was created. Take good care of your ${zoo.animals.length} animals!`; + const responseMessage = ` +Your nice ${zoo.name} Zoo was created. + +Take good care of your ${zoo.animals.length} animals! +All those ${ + distinctBy(zoo.animals, (it) => it.kind).map((it) => `${it.kind}s`).join( + " and ", + ) + } will surely amaze your visitors. +`; return new Response(responseMessage, { status: 201, @@ -21,19 +31,23 @@ const handleCreateZoo = stack(validateZoo) .add(createZoo) .handler; -const handler = router(new Map([ +const handler = router( + new Map([ [ - [ /^\/zoos\/yaml$/, 'POST' ], - stack(acceptYaml) - .add(handleCreateZoo) - .handler, + [/^\/zoos\/yaml$/, "POST"], + stack(log) + .add(acceptYaml) + .add(handleCreateZoo) + .handler, ], [ - [ /^\/zoos\/json/, 'POST' ], - stack(acceptJson) - .add(handleCreateZoo) - .handler, + [/^\/zoos\/json/, "POST"], + stack(log) + .add(acceptJson) + .add(handleCreateZoo) + .handler, ], -])) + ]), +); -await listenAndServe('', handler) +await listenAndServe(":5000", handler); diff --git a/http/middleware/poc/validate_zoo_zonfig.ts b/http/middleware/poc/validate_zoo_zonfig.ts index d0c39f5e322d..9e5d712e746f 100644 --- a/http/middleware/poc/validate_zoo_zonfig.ts +++ b/http/middleware/poc/validate_zoo_zonfig.ts @@ -1,35 +1,38 @@ -import { Middleware } from '../../middleware.ts' -import { includesValue } from '../../../collections/includes_value.ts' -import { AnimalKind, Zoo } from './zoo.ts' +import { Middleware } from "../../middleware.ts"; +import { includesValue } from "../../../collections/includes_value.ts"; +import { AnimalKind, Zoo } from "./zoo.ts"; function isZoo(subject: unknown): subject is Zoo { - const cast = subject as Zoo + const cast = subject as Zoo; - return typeof cast === 'object' - && typeof cast.name ==='string' - && typeof cast.entryFee === 'number' - && Array.isArray(cast.animals) - && (cast.animals as unknown[]).every(isAnimalKind) + return typeof cast === "object" && + typeof cast.name === "string" && + typeof cast.entryFee === "number" && + Array.isArray(cast.animals) && + (cast.animals as unknown[]).every(isAnimalKind); } function isAnimalKind(subject: unknown): subject is AnimalKind { - return typeof subject === 'string' && includesValue(AnimalKind, subject) + return typeof subject === "string" && includesValue(AnimalKind, subject); } -export const validateZoo: Middleware = async (req, next) => { - const { parsedBody } = req +export const validateZoo: Middleware< + Request & { parsedBody: unknown }, + { zoo: Zoo } +> = async (req, con, next) => { + const { parsedBody } = req; - if (!isZoo(parsedBody)) { - return new Response(null, { - status: 422, - statusText: 'Invalid ZooConfig', - }) - } + if (!isZoo(parsedBody)) { + return new Response(null, { + status: 422, + statusText: "Invalid ZooConfig", + }); + } - const nextReq = { - ...req, - zoo: parsedBody, - } + const nextReq = { + ...req, + zoo: parsedBody, + }; - return next!(nextReq) -} + return next!(nextReq, con); +}; diff --git a/http/middleware/poc/zoo.ts b/http/middleware/poc/zoo.ts index 1556f18f2445..cf88256b0ad6 100644 --- a/http/middleware/poc/zoo.ts +++ b/http/middleware/poc/zoo.ts @@ -1,17 +1,16 @@ export enum AnimalKind { - Tiger = 'Tiger', - Elephant = 'Elephant', - RedPanda = 'Red Panda', - Monkey = 'Monkey', - Hippo = 'Hippo', + Tiger = "Tiger", + Elephant = "Elephant", + RedPanda = "Red Panda", + Monkey = "Monkey", + Hippo = "Hippo", } export type Zoo = { - name: string - entryFee: number - animals: { - name: string - kind: AnimalKind - }[] -} - + name: string; + entryFee: number; + animals: { + name: string; + kind: AnimalKind; + }[]; +}; diff --git a/http/middleware/yaml.ts b/http/middleware/yaml.ts index 6c9b1b112cb8..2e73e9ad10d0 100644 --- a/http/middleware/yaml.ts +++ b/http/middleware/yaml.ts @@ -1,29 +1,33 @@ -import { Middleware } from '../middleware.ts' -import { parse } from "../../encoding/yaml.ts" -import { YAMLError } from "../../encoding/_yaml/error.ts" +import { Middleware } from "../middleware.ts"; +import { parse } from "../../encoding/yaml.ts"; +import { YAMLError } from "../../encoding/_yaml/error.ts"; -export const acceptYaml: Middleware = async (req, next) => { - const body = await req.text() +export const acceptYaml: Middleware = async ( + req, + con, + next, +) => { + const body = await req.text(); - let parsedBody: unknown + let parsedBody: unknown; - try { - parsedBody = parse(body) - } catch(e) { - if (e instanceof YAMLError) { - return new Response( - e.toString(false), - { status: 422, statusText: 'Request could not be parsed' } - ) - } - - throw e + try { + parsedBody = parse(body); + } catch (e) { + if (e instanceof YAMLError) { + return new Response( + e.toString(false), + { status: 422, statusText: "Request could not be parsed" }, + ); } - const nextReq = { - ...req, - parsedBody, - } + throw e; + } + + const nextReq = { + ...req, + parsedBody, + }; - return next!(nextReq) -} + return next!(nextReq, con); +}; diff --git a/http/playground.ts b/http/playground.ts deleted file mode 100644 index b63789be1d0b..000000000000 --- a/http/playground.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { assertEquals } from "../testing/asserts.ts"; -import { HttpRequest, Middleware, stack } from "./middleware.ts"; - -type AuthedRequest = HttpRequest & { auth: string }; - -const passThrough: Middleware = async (req, next) => next!(req); - -const authenticate: Middleware = async ( - req, - next, -) => { - const auth = req.path; - const response = await next!({ - ...req, - auth, - }); - - return response; -}; - -const authorize = async ( - req: R, - next?: Middleware, -) => { - const isAuthorized = req.auth === "admin"; - - if (!isAuthorized) { - return { body: "nope" }; - } - - return next!(req); -}; - -const rainbow: Middleware = async ( - req, - next, -) => next!({ ...req, rainbow: true }); - -const handleGet: Middleware = async (req) => ({ body: "yey" }); - -const combinedHandler = stack(passThrough) - .add(authenticate) - .add(authorize) - .add(rainbow) - .add(handleGet) - .handler; - -assertEquals(await combinedHandler({ path: "someuser" }), { body: "nope" }); -assertEquals(await combinedHandler({ path: "admin" }), { body: "yey" }); diff --git a/http/router.ts b/http/router.ts index 81fef4d4463d..ed642311c54f 100644 --- a/http/router.ts +++ b/http/router.ts @@ -1,15 +1,17 @@ -import { Handler } from './mod.ts' +import { Handler } from "./mod.ts"; -export function router(routes: Map<[RegExp, Request['method']], Handler>): Handler { - return async (req, con) => { - const { pathname: path } = new URL(req.url) +export function router( + routes: Map<[RegExp, Request["method"]], Handler>, +): Handler { + return async (req, con) => { + const { pathname: path } = new URL(req.url); - for (const [ [ pattern, method ], handler ] of routes) { - if (req.method === method && pattern.exec(path) !== null) { - return await handler(req, con) - } - } - - return new Response(null, { status: 404, statusText: 'Route not found' }) + for (const [[pattern, method], handler] of routes) { + if (req.method === method && pattern.exec(path) !== null) { + return await handler(req, con); + } } + + return new Response(null, { status: 404, statusText: "Route not found" }); + }; } From 97d49251d4bf4fa87485812240ceb6a94b1f7745 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 19 Sep 2021 18:24:58 +0200 Subject: [PATCH 07/84] :construction: --- http/middleware.ts | 4 +- http/middleware/json.ts | 4 +- http/middleware/poc/server.ts | 33 +++------------- http/middleware/poc/validate_zoo_zonfig.ts | 2 +- http/middleware/router.ts | 46 ++++++++++++++++++++++ 5 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 http/middleware/router.ts diff --git a/http/middleware.ts b/http/middleware.ts index 72bd6008b3fe..84400ca322a0 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -19,10 +19,10 @@ type MiddlewareStack< add( middleware: Middleware, - ): MiddlewareStack; + ): MiddlewareStack; }; -function addMiddleware< +export function addMiddleware< StackAdd, HandlerAdd, >( diff --git a/http/middleware/json.ts b/http/middleware/json.ts index 918f1ff20e20..6d37f6bbc493 100644 --- a/http/middleware/json.ts +++ b/http/middleware/json.ts @@ -14,8 +14,8 @@ export const acceptJson: Middleware = async ( } catch (e) { if (e instanceof SyntaxError) { return new Response( - e.message, - { status: 422, statusText: "Request could not be parsed" }, + `Request could not be parsed as JSON: ${e.message}`, + { status: 422 }, ); } diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index c4407c0e538e..af6ff612af36 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -1,14 +1,14 @@ -import { router } from "../../router.ts"; +import { route } from "../router.ts" import { distinctBy } from "../../../collections/mod.ts"; import { listenAndServe } from "../../server.ts"; import { acceptYaml } from "../yaml.ts"; import { acceptJson } from "../json.ts"; import { log } from "../log.ts"; -import { stack } from "../../middleware.ts"; +import { stack, addMiddleware } from "../../middleware.ts"; import { validateZoo } from "./validate_zoo_zonfig.ts"; import { Zoo } from "./zoo.ts"; -async function createZoo(req: Request & { zoo: Zoo }) { +function createZoo(req: Request & { zoo: Zoo }) { const { zoo } = req; const responseMessage = ` Your nice ${zoo.name} Zoo was created. @@ -21,33 +21,12 @@ All those ${ } will surely amaze your visitors. `; - return new Response(responseMessage, { - status: 201, - statusText: "Zoo created", - }); + return Promise.resolve(new Response(responseMessage, { status: 201 })); } const handleCreateZoo = stack(validateZoo) .add(createZoo) .handler; -const handler = router( - new Map([ - [ - [/^\/zoos\/yaml$/, "POST"], - stack(log) - .add(acceptYaml) - .add(handleCreateZoo) - .handler, - ], - [ - [/^\/zoos\/json/, "POST"], - stack(log) - .add(acceptJson) - .add(handleCreateZoo) - .handler, - ], - ]), -); - -await listenAndServe(":5000", handler); +addMiddleware(log, handleCreateZoo) +//await listenAndServe(":5000", handler); diff --git a/http/middleware/poc/validate_zoo_zonfig.ts b/http/middleware/poc/validate_zoo_zonfig.ts index 9e5d712e746f..f7824c6facd4 100644 --- a/http/middleware/poc/validate_zoo_zonfig.ts +++ b/http/middleware/poc/validate_zoo_zonfig.ts @@ -34,5 +34,5 @@ export const validateZoo: Middleware< zoo: parsedBody, }; - return next!(nextReq, con); + return await next!(nextReq, con); }; diff --git a/http/middleware/router.ts b/http/middleware/router.ts new file mode 100644 index 000000000000..c286b8785e79 --- /dev/null +++ b/http/middleware/router.ts @@ -0,0 +1,46 @@ +import { Middleware } from "../middleware.ts"; + +export type Routes = { + [pattern: string]: { + [method: string]: Middleware; + }; +}; + +export function route(routes: Routes): Middleware { + const patternMap = new Map( + Object + .entries(routes) + .map(([pattern, methods]) => [new URLPattern({ pathname: pattern }), methods]), + ); + + const patterns = [ ...patternMap.keys() ]; + + return async (req, con) => { + const matchedPattern = patterns.find((it) => it.exec(req.url)); + + if (matchedPattern === undefined) { + return new Response(`${path} did not match any routes`, { status: 404 }); + } + + const handler = patternMap.get(matchedPattern)![req.method]; + + if (handler === undefined) { + const supportedMethods = Object + .keys(patternMap.get(matchedPattern)!) + .map((it) => it.toUpperCase()) + .join(", "); + + return new Response( + `Method ${req.method} is not allowed for this route. Supported methods are: ${supportedMethods}`, + { + status: 404, + headers: { + "Allow": supportedMethods, + }, + }, + ); + } + + return await handler(req, con); + }; +} From f0663835448c8665ce61fc7ab127761780ccbf91 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 19 Sep 2021 22:31:47 +0200 Subject: [PATCH 08/84] :construction: --- http/middleware.ts | 25 ++++++++++++++++--------- http/playground.ts | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 http/playground.ts diff --git a/http/middleware.ts b/http/middleware.ts index 84400ca322a0..dabe792b52a9 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -17,23 +17,30 @@ type MiddlewareStack< > = { handler: Middleware; - add( - middleware: Middleware, - ): MiddlewareStack; + add( + middleware: Middleware, + ): MiddlewareStack, Adds & AddedAdds>; }; export function addMiddleware< - StackAdd, - HandlerAdd, + FirstRequires extends Request, + FirstAdd, + SecondRequires extends Request, + SecondAdd, >( - first: Middleware, - second: Middleware, -): Middleware { + first: Middleware, + second: Middleware, +): Middleware, FirstAdd & SecondAdd> { return (req, con, next) => first( req, con, - (r) => second(r, con, next), + (r) => second( + //@ts-ignore: TS does not know about the middleware magic + r, + con, + next + ), ); } diff --git a/http/playground.ts b/http/playground.ts new file mode 100644 index 000000000000..8ae84dfeb841 --- /dev/null +++ b/http/playground.ts @@ -0,0 +1,39 @@ +import { addMiddleware, stack, Middleware } from './middleware.ts' +import { Handler } from './mod.ts' + +type AuthedRequest = Request & { auth: string } + +const passThrough: Middleware = (req, con, next) => next!(req, con) + +const authenticate: Middleware = async (req, con, next) => { + const auth = req.headers.get('authorization') + + if (auth === null) { + return new Response(null, { status: 401 }) + } + + return await next!({ ...req, auth }, con) +} + +const authorize: Middleware = async (req, con, next) => { + const { auth } = req + + if (auth !== 'asdf') { + return new Response(null, { status: 401 }) + } + + return await next!(req, con) +} + +const handle: Middleware = async (req: AuthedRequest) => { + return new Response(`Hi ${req.auth}`, { status: 200 }) +} + +const test = stack(passThrough) + .add(authenticate) + .add(authorize) + .add(handle) + .handler + + +const http: Handler = test From b5eee720292fea65f3c25e7bad45256df9d8a53c Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 14:27:51 +0200 Subject: [PATCH 09/84] :construction: --- http/middleware.ts | 19 ++- http/middleware/json.ts | 7 + http/middleware/poc/server.ts | 16 +- http/middleware/poc/validate_zoo_zonfig.ts | 17 +- http/middleware/poc/zoo.ts | 10 +- http/middleware/rfc.md | 185 +++++++++++++++++++++ http/middleware/router.ts | 6 +- http/playground.ts | 53 +++--- 8 files changed, 262 insertions(+), 51 deletions(-) create mode 100644 http/middleware/rfc.md diff --git a/http/middleware.ts b/http/middleware.ts index dabe792b52a9..a07689c66e04 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -19,7 +19,10 @@ type MiddlewareStack< add( middleware: Middleware, - ): MiddlewareStack, Adds & AddedAdds>; + ): MiddlewareStack< + Requires & Omit, + Adds & AddedAdds + >; }; export function addMiddleware< @@ -30,17 +33,21 @@ export function addMiddleware< >( first: Middleware, second: Middleware, -): Middleware, FirstAdd & SecondAdd> { +): Middleware< + FirstRequires & Omit, + FirstAdd & SecondAdd +> { return (req, con, next) => first( req, con, - (r) => second( - //@ts-ignore: TS does not know about the middleware magic + (r) => + second( + //@ts-ignore: TS does not know about the middleware magic here r, con, - next - ), + next, + ), ); } diff --git a/http/middleware/json.ts b/http/middleware/json.ts index 6d37f6bbc493..4493948c8bdc 100644 --- a/http/middleware/json.ts +++ b/http/middleware/json.ts @@ -5,6 +5,13 @@ export const acceptJson: Middleware = async ( con, next, ) => { + if (!req.headers.get("content-type")?.includes("application/json")) { + return new Response( + "Content Type not supported, expected application/json", + { status: 415 }, + ); + } + const body = await req.text(); let parsedBody: unknown; diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index af6ff612af36..a7b815db966e 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -1,14 +1,13 @@ -import { route } from "../router.ts" +//import { route } from "../router.ts"; import { distinctBy } from "../../../collections/mod.ts"; import { listenAndServe } from "../../server.ts"; -import { acceptYaml } from "../yaml.ts"; import { acceptJson } from "../json.ts"; import { log } from "../log.ts"; -import { stack, addMiddleware } from "../../middleware.ts"; +import { Middleware, stack } from "../../middleware.ts"; import { validateZoo } from "./validate_zoo_zonfig.ts"; import { Zoo } from "./zoo.ts"; -function createZoo(req: Request & { zoo: Zoo }) { +const createZoo: Middleware = (req, con) => { const { zoo } = req; const responseMessage = ` Your nice ${zoo.name} Zoo was created. @@ -22,11 +21,12 @@ All those ${ `; return Promise.resolve(new Response(responseMessage, { status: 201 })); -} +}; -const handleCreateZoo = stack(validateZoo) +const handleCreateZoo = stack(log) + .add(acceptJson) + .add(validateZoo) .add(createZoo) .handler; -addMiddleware(log, handleCreateZoo) -//await listenAndServe(":5000", handler); +await listenAndServe("0.0.0.0:5000", handleCreateZoo); diff --git a/http/middleware/poc/validate_zoo_zonfig.ts b/http/middleware/poc/validate_zoo_zonfig.ts index f7824c6facd4..aaff9102ddf1 100644 --- a/http/middleware/poc/validate_zoo_zonfig.ts +++ b/http/middleware/poc/validate_zoo_zonfig.ts @@ -1,6 +1,6 @@ import { Middleware } from "../../middleware.ts"; import { includesValue } from "../../../collections/includes_value.ts"; -import { AnimalKind, Zoo } from "./zoo.ts"; +import { Animal, AnimalKind, Zoo } from "./zoo.ts"; function isZoo(subject: unknown): subject is Zoo { const cast = subject as Zoo; @@ -9,7 +9,15 @@ function isZoo(subject: unknown): subject is Zoo { typeof cast.name === "string" && typeof cast.entryFee === "number" && Array.isArray(cast.animals) && - (cast.animals as unknown[]).every(isAnimalKind); + (cast.animals as unknown[]).every(isAnimal); +} + +function isAnimal(subject: unknown): subject is Animal { + const cast = subject as Animal; + + return typeof cast === "object" && + typeof cast.name === "string" && + isAnimalKind(cast.kind as unknown); } function isAnimalKind(subject: unknown): subject is AnimalKind { @@ -23,10 +31,7 @@ export const validateZoo: Middleware< const { parsedBody } = req; if (!isZoo(parsedBody)) { - return new Response(null, { - status: 422, - statusText: "Invalid ZooConfig", - }); + return new Response("Invalid Zoo", { status: 422 }); } const nextReq = { diff --git a/http/middleware/poc/zoo.ts b/http/middleware/poc/zoo.ts index cf88256b0ad6..f8b0aa8ca234 100644 --- a/http/middleware/poc/zoo.ts +++ b/http/middleware/poc/zoo.ts @@ -6,11 +6,13 @@ export enum AnimalKind { Hippo = "Hippo", } +export type Animal = { + name: string; + kind: AnimalKind; +}; + export type Zoo = { name: string; entryFee: number; - animals: { - name: string; - kind: AnimalKind; - }[]; + animals: Animal[]; }; diff --git a/http/middleware/rfc.md b/http/middleware/rfc.md new file mode 100644 index 000000000000..9d2026ffcf4e --- /dev/null +++ b/http/middleware/rfc.md @@ -0,0 +1,185 @@ +# `std/http/middleware` Concept + +## Goals + +- **Establish a middleware concept that enables `std/http` to be used for actual + applications** directly in the future. Once a pattern is established, there + are already some modules in `std` that could easily be wrapped into + out-of-the-box middleware +- **Allow middleware and composed middleware stacks to just be a function** that + takes some form of request and returns a response, optionally calling the next + middleware. This ensures that we deal with normal call stacks, allows errors + to bubble up as expected and reduces the amount of black magic happening at + runtime +- **Be completely type safe, including middleware order and arbitrary middleware + composition.** This means I want the type checker to stop me from registering + a handler on the server that assumes that certain information is available + from previous middlewares (e.g. auth info, parsed and validated bodies...), + even though that middleware is not present in that handler's chain. Just + having a global state that can be typed and is assumed to always be present in + every function is not good enough - we are dealing with a chain of functions + here, we should leverage Typescript and make sure that that chain actually + works type-wise. +- The middleware signature should **be compatible to the `Server`s `Handler` + signature**. Composing middleware should always just return a new middleware, + so that compositions can be modularized and passed around opaquely + +## POC + +TODO + +I have linked a branch in which I have built a small POC fullfiling the goals +above. **This is just to show the idea**. It is not fleshed out, very rough +around a lot of edged, has subpar ergonomics and several straight up bugs. All +of them are solvable in several ways and the solution is not vital to the +concept, so I left them as they are for the sake of discussion. + +I stopped writing as soon as I was sure enough that this can be done. There are +many ways to do this basic concept and a lot of them are viable - I did not want +to invest into one of them, just have something to start talking. + +## POC + +### API + +The POC contains three components. Their actual runtime code is really small - +most of the code around it (and most todos to fix the bugs / ergonomics issues) +is just types. + +The components are: + +- A `Middleware` function type with two important generic type parameters: + - What type the middleware expects (e.g. it needs a semantic auth field on top + of the normal request) + - Optionally, what information the middleware adds to the request "context" + (e.g. validating the body to be a valid `Animal` and adding an + `animal: Animal` property) It could be used like this (lots of abstracted + ideas in there to show the idea): + + ```typescript + const validateAnimal: Middleware = async ( + req, + con, + next, + ) => { + const body = extractBody(req); + + if (!isAnimal(body)) { + return new Response( + "Invalid Animal", + { status: 422 }, + ); + } + + const nextReq = extend(req, { animal: body }); + + return await next!(nextReq, con); + }; + ``` +- A `composeMiddleware` function that takes two `Middleware`s and returns a new + `Middleware` that is a composition of both in the given order. The resulting + `Middleware` adds a union of what both arguments add and requires a union of + what both arguments require, except the intersection between what the first + one adds and the second one requires, as that has already been satisfied + within the composition. + + It could be used like that: + + ```typescript + declare const authenticate: Middleware; + declare const authorize: Middleware; + + const checkAccess = composeMiddleware(authenticate, authorize); + + assertType>(checkAccess); + ``` + + `composeMiddleware` is the atomic composition and type checking step but not + very ergonomic to use, as it can only handle two middlewares being combined. +- A `stack` helper that wraps a given `Middleware` in an object thas has a + chainable `.add()` method. This allows for nicer usage and follows the usual + `.use()` idea in spirit. It can used like this: + + ```typescript + declare const authenticate: Middleware; + declare const authorize: Middleware; + declare const validateAnimal: Middleware; + + const authAndValidate = stack(authenticate) + .add(authorize) + .add(validateAnimal) + .handler; + + assertType>( + authAndValidate, + ); + ``` + + This essentially just wraps `composeMiddleware` to be chainable with correct + typing. + + Notice the `.handler` at the end - this extracts the actual function again. + There might be nicer ways to do it, but the concept works for the sake of + discussion. + +The components above fulfill the goals mentioned above: + +- `Middleware` is just a function, including the result of an arbitrary + `stack().add().add().add().handler` chain +- `Middleware is assignable to`std/http``Handler` - meaning there is no + additional wrapping necessary +- Middleware composition is completely type safe and order-aware. This means + that all requirements that are present but not fulfilled by previous + middleware "bubbles up" and will type error when trying to register it on the + `Server`, stating which properties are missing + + To be fair, it makes some assumptions. It assumes that you always add the same + type to your `next` call, so if you have conditional calls, you need to + "flatten" the types. It also assumes that you do not throw away the previous + request context. However, I think those are reasonable assumptions and they + are also present (and a lot less safe) in the current TS middleware concepts + e.g. in koa / oak. + +### Play around with it + +To run a small server with some middleware from the POC branch, follow the steps +below. **The implemented middleware is just for presentation purposes**, it's +implementation is very bad, but it works to show the idea. + +1. Check out the branch, e.g. with + `$ git remote add lionc git@github.com:LionC/deno_std.git && git fetch && git switch middleware-experiment` +2. Start the server with `$ deno run --allow-net http/middleware/poc/server.ts` +3. Throw some requests at it, here are some `httpie` example commands: + +- Succeed (without any animals): `http --json 0.0.0.0:5000/zoos/json name=My entryFee:=10 animals:='[{"name": "Kim", "kind": "Tiger"}, {"name": "Flippo", "kind": "Hippo"}, {"name": "Jasmin", "kind": "Tiger"}]'` +- Fail validation: `$ http --json 0.0.0.0:5000/zoos/json name=My entryFee:=10` +- Fail JSON content type: `$ http --form 0.0.0.0:5000/zoos/json name=My entryFee:=10` + +`http/middleware/poc/server.ts` is also a good place to play around with the type safe composition - try changing the order of middleware, leave a vital one out and see how LSP / tsc react. + +## What now? + +There are several questions to answer here: + +- What have I missed? Is this something we want to go deeper on? +- How do we want the API for application and middleware authors to look like? + See my take on `Request` below. The pattern above works either way, but I + think we should take a look at that. + +### On `Request` and API ergonomic + +While working on this and trying to write some middlewares, I really felt that +the current `Handler` signature is quite...weird. I get why it looks that way, +but from an API perspective, it does not make a lot of sense that two arbitrary +fields about the incoming request are separated into their own argument. It also +does not make a lot of sense that some arbitrary functionality that would be +expected on the request parameter needs to be separately `import`ed as a +function and called on that object. There is also not really a nice way to add +new things to a request in a type safe way. + +Following `Request` makes a lot of sense, it being a Web standard and all. But I +think it could make sense to `extend` `Request` in `std/http` to have one +central API for everything concerning the incoming request - including +`connInfo`, a simple helper to add to some kind of request context, helpers to +get common info like parsed content types, get cookies etc while still following +`Request` for everything it offers. diff --git a/http/middleware/router.ts b/http/middleware/router.ts index c286b8785e79..c8e1152094ee 100644 --- a/http/middleware/router.ts +++ b/http/middleware/router.ts @@ -10,10 +10,12 @@ export function route(routes: Routes): Middleware { const patternMap = new Map( Object .entries(routes) - .map(([pattern, methods]) => [new URLPattern({ pathname: pattern }), methods]), + .map(( + [pattern, methods], + ) => [new URLPattern({ pathname: pattern }), methods]), ); - const patterns = [ ...patternMap.keys() ]; + const patterns = [...patternMap.keys()]; return async (req, con) => { const matchedPattern = patterns.find((it) => it.exec(req.url)); diff --git a/http/playground.ts b/http/playground.ts index 8ae84dfeb841..a48d986b361c 100644 --- a/http/playground.ts +++ b/http/playground.ts @@ -1,39 +1,42 @@ -import { addMiddleware, stack, Middleware } from './middleware.ts' -import { Handler } from './mod.ts' +import { addMiddleware, Middleware, stack } from "./middleware.ts"; +import { Handler } from "./mod.ts"; -type AuthedRequest = Request & { auth: string } +type AuthedRequest = Request & { auth: string }; -const passThrough: Middleware = (req, con, next) => next!(req, con) +const passThrough: Middleware = (req, con, next) => next!(req, con); -const authenticate: Middleware = async (req, con, next) => { - const auth = req.headers.get('authorization') +const authenticate: Middleware = async ( + req, + con, + next, +) => { + const auth = req.headers.get("authorization"); - if (auth === null) { - return new Response(null, { status: 401 }) - } + if (auth === null) { + return new Response(null, { status: 401 }); + } - return await next!({ ...req, auth }, con) -} + return await next!({ ...req, auth }, con); +}; const authorize: Middleware = async (req, con, next) => { - const { auth } = req + const { auth } = req; - if (auth !== 'asdf') { - return new Response(null, { status: 401 }) - } + if (auth !== "asdf") { + return new Response(null, { status: 401 }); + } - return await next!(req, con) -} + return await next!(req, con); +}; const handle: Middleware = async (req: AuthedRequest) => { - return new Response(`Hi ${req.auth}`, { status: 200 }) -} + return new Response(`Hi ${req.auth}`, { status: 200 }); +}; const test = stack(passThrough) - .add(authenticate) - .add(authorize) - .add(handle) - .handler + .add(authenticate) + .add(authorize) + .add(handle) + .handler; - -const http: Handler = test +const http: Handler = test; From 9291218484c0c862cc7440374184ed87d0cac34f Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 14:38:21 +0200 Subject: [PATCH 10/84] :construction: --- http/middleware/log.ts | 2 +- http/middleware/poc/server.ts | 5 +- ...validate_zoo_zonfig.ts => validate_zoo.ts} | 0 http/middleware/rfc.md | 6 +-- http/middleware/router.ts | 48 ------------------- http/middleware/yaml.ts | 33 ------------- 6 files changed, 6 insertions(+), 88 deletions(-) rename http/middleware/poc/{validate_zoo_zonfig.ts => validate_zoo.ts} (100%) delete mode 100644 http/middleware/router.ts delete mode 100644 http/middleware/yaml.ts diff --git a/http/middleware/log.ts b/http/middleware/log.ts index 1083165f2308..89eda95fbd51 100644 --- a/http/middleware/log.ts +++ b/http/middleware/log.ts @@ -6,7 +6,7 @@ export const log: Middleware = async (req, con, next) => { const end = performance.now(); console.log( - `${req.method} ${new URL(req.url).pathname}\t\t${response.status}\t\t${end - + `${req.method} ${new URL(req.url).pathname}\t${response.status}\t${end - start}ms`, ); diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index a7b815db966e..fffe33c776e7 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -1,13 +1,12 @@ -//import { route } from "../router.ts"; import { distinctBy } from "../../../collections/mod.ts"; import { listenAndServe } from "../../server.ts"; import { acceptJson } from "../json.ts"; import { log } from "../log.ts"; import { Middleware, stack } from "../../middleware.ts"; -import { validateZoo } from "./validate_zoo_zonfig.ts"; +import { validateZoo } from "./validate_zoo.ts"; import { Zoo } from "./zoo.ts"; -const createZoo: Middleware = (req, con) => { +const createZoo: Middleware = (req) => { const { zoo } = req; const responseMessage = ` Your nice ${zoo.name} Zoo was created. diff --git a/http/middleware/poc/validate_zoo_zonfig.ts b/http/middleware/poc/validate_zoo.ts similarity index 100% rename from http/middleware/poc/validate_zoo_zonfig.ts rename to http/middleware/poc/validate_zoo.ts diff --git a/http/middleware/rfc.md b/http/middleware/rfc.md index 9d2026ffcf4e..550ff8ceca5a 100644 --- a/http/middleware/rfc.md +++ b/http/middleware/rfc.md @@ -151,9 +151,9 @@ implementation is very bad, but it works to show the idea. 2. Start the server with `$ deno run --allow-net http/middleware/poc/server.ts` 3. Throw some requests at it, here are some `httpie` example commands: -- Succeed (without any animals): `http --json 0.0.0.0:5000/zoos/json name=My entryFee:=10 animals:='[{"name": "Kim", "kind": "Tiger"}, {"name": "Flippo", "kind": "Hippo"}, {"name": "Jasmin", "kind": "Tiger"}]'` -- Fail validation: `$ http --json 0.0.0.0:5000/zoos/json name=My entryFee:=10` -- Fail JSON content type: `$ http --form 0.0.0.0:5000/zoos/json name=My entryFee:=10` +- Succeed (without any animals): `http --json 0.0.0.0:5000/ name=My entryFee:=10 animals:='[{"name": "Kim", "kind": "Tiger"}, {"name": "Flippo", "kind": "Hippo"}, {"name": "Jasmin", "kind": "Tiger"}]'` +- Fail validation: `$ http --json 0.0.0.0:5000/ name=My entryFee:=10` +- Fail JSON content type: `$ http --form 0.0.0.0:5000/ name=My entryFee:=10` `http/middleware/poc/server.ts` is also a good place to play around with the type safe composition - try changing the order of middleware, leave a vital one out and see how LSP / tsc react. diff --git a/http/middleware/router.ts b/http/middleware/router.ts deleted file mode 100644 index c8e1152094ee..000000000000 --- a/http/middleware/router.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Middleware } from "../middleware.ts"; - -export type Routes = { - [pattern: string]: { - [method: string]: Middleware; - }; -}; - -export function route(routes: Routes): Middleware { - const patternMap = new Map( - Object - .entries(routes) - .map(( - [pattern, methods], - ) => [new URLPattern({ pathname: pattern }), methods]), - ); - - const patterns = [...patternMap.keys()]; - - return async (req, con) => { - const matchedPattern = patterns.find((it) => it.exec(req.url)); - - if (matchedPattern === undefined) { - return new Response(`${path} did not match any routes`, { status: 404 }); - } - - const handler = patternMap.get(matchedPattern)![req.method]; - - if (handler === undefined) { - const supportedMethods = Object - .keys(patternMap.get(matchedPattern)!) - .map((it) => it.toUpperCase()) - .join(", "); - - return new Response( - `Method ${req.method} is not allowed for this route. Supported methods are: ${supportedMethods}`, - { - status: 404, - headers: { - "Allow": supportedMethods, - }, - }, - ); - } - - return await handler(req, con); - }; -} diff --git a/http/middleware/yaml.ts b/http/middleware/yaml.ts deleted file mode 100644 index 2e73e9ad10d0..000000000000 --- a/http/middleware/yaml.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Middleware } from "../middleware.ts"; -import { parse } from "../../encoding/yaml.ts"; -import { YAMLError } from "../../encoding/_yaml/error.ts"; - -export const acceptYaml: Middleware = async ( - req, - con, - next, -) => { - const body = await req.text(); - - let parsedBody: unknown; - - try { - parsedBody = parse(body); - } catch (e) { - if (e instanceof YAMLError) { - return new Response( - e.toString(false), - { status: 422, statusText: "Request could not be parsed" }, - ); - } - - throw e; - } - - const nextReq = { - ...req, - parsedBody, - }; - - return next!(nextReq, con); -}; From 9ef07b8afa4ff037fb23c535a48b5789b1dd7ac8 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 14:39:21 +0200 Subject: [PATCH 11/84] :construction: --- http/middleware/{rfc.md => discussion.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename http/middleware/{rfc.md => discussion.md} (100%) diff --git a/http/middleware/rfc.md b/http/middleware/discussion.md similarity index 100% rename from http/middleware/rfc.md rename to http/middleware/discussion.md From 46359714d5eb13f914e3301bdd4519699786b9d6 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 14:43:58 +0200 Subject: [PATCH 12/84] :memo: --- http/middleware/discussion.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/http/middleware/discussion.md b/http/middleware/discussion.md index 550ff8ceca5a..bced6cc22e22 100644 --- a/http/middleware/discussion.md +++ b/http/middleware/discussion.md @@ -26,19 +26,17 @@ ## POC -TODO - -I have linked a branch in which I have built a small POC fullfiling the goals -above. **This is just to show the idea**. It is not fleshed out, very rough -around a lot of edged, has subpar ergonomics and several straight up bugs. All -of them are solvable in several ways and the solution is not vital to the -concept, so I left them as they are for the sake of discussion. - -I stopped writing as soon as I was sure enough that this can be done. There are -many ways to do this basic concept and a lot of them are viable - I did not want -to invest into one of them, just have something to start talking. - -## POC +[Here is a branch](https://github.com/LionC/deno_std/tree/middleware-experiment/http) in +which I have built a small dirty POC fullfiling the goals above. **This is just +to show the idea**. It is not fleshed out, very rough around a lot of edges, +has subpar ergonomics and several straight up bugs. All of them are solvable in +several ways and their solution is not vital to the concept, so I left them as +they are for the sake of starting a conversation. + +I stopped writing as soon as I was sure enough that this can be done +reasonably. There are many ways to do this basic concept and a lot of them are +viable - I did not want to invest into one of them, just have something to +start talking. ### API From af1fab286050459a28716f2c3bbb8c013b05c0be Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 14:44:56 +0200 Subject: [PATCH 13/84] :memo: --- http/middleware/discussion.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/http/middleware/discussion.md b/http/middleware/discussion.md index bced6cc22e22..39fad3fa2916 100644 --- a/http/middleware/discussion.md +++ b/http/middleware/discussion.md @@ -157,9 +157,11 @@ implementation is very bad, but it works to show the idea. ## What now? -There are several questions to answer here: +There are two questions to answer here: -- What have I missed? Is this something we want to go deeper on? +- What have I missed? Is this something we want to go deeper on? I did not want + to invest more time into figuring out all the details before there is some + input on the core idea - How do we want the API for application and middleware authors to look like? See my take on `Request` below. The pattern above works either way, but I think we should take a look at that. From 0ae8fbe44f8f33a258fc4529b54a5ecbab96bb17 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 14:46:37 +0200 Subject: [PATCH 14/84] :memo: --- http/middleware/discussion.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/http/middleware/discussion.md b/http/middleware/discussion.md index 39fad3fa2916..1f59f243915c 100644 --- a/http/middleware/discussion.md +++ b/http/middleware/discussion.md @@ -55,21 +55,17 @@ The components are: ideas in there to show the idea): ```typescript - const validateAnimal: Middleware = async ( - req, - con, - next, - ) => { + const validateFoo: Middleware = async (req, con, next) => { const body = extractBody(req); - if (!isAnimal(body)) { + if (!isFoo(body)) { return new Response( - "Invalid Animal", + "Invalid Foo", { status: 422 }, ); } - const nextReq = extend(req, { animal: body }); + const nextReq = extend(req, { foo: body }); return await next!(nextReq, con); }; @@ -101,11 +97,11 @@ The components are: ```typescript declare const authenticate: Middleware; declare const authorize: Middleware; - declare const validateAnimal: Middleware; + declare const validateFoo: Middleware; const authAndValidate = stack(authenticate) .add(authorize) - .add(validateAnimal) + .add(validateFoo) .handler; assertType>( From 06ea2e46ba79075cb964292eccfa391d4d8b0304 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 15:02:48 +0200 Subject: [PATCH 15/84] :memo: --- http/middleware/discussion.md | 57 ++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/http/middleware/discussion.md b/http/middleware/discussion.md index 1f59f243915c..7ed477ba0dd4 100644 --- a/http/middleware/discussion.md +++ b/http/middleware/discussion.md @@ -52,7 +52,7 @@ The components are: - Optionally, what information the middleware adds to the request "context" (e.g. validating the body to be a valid `Animal` and adding an `animal: Animal` property) It could be used like this (lots of abstracted - ideas in there to show the idea): + functions in here to show the idea): ```typescript const validateFoo: Middleware = async (req, con, next) => { @@ -92,7 +92,7 @@ The components are: very ergonomic to use, as it can only handle two middlewares being combined. - A `stack` helper that wraps a given `Middleware` in an object thas has a chainable `.add()` method. This allows for nicer usage and follows the usual - `.use()` idea in spirit. It can used like this: + `.use()` idea in spirit. It can be used like this: ```typescript declare const authenticate: Middleware; @@ -120,19 +120,19 @@ The components above fulfill the goals mentioned above: - `Middleware` is just a function, including the result of an arbitrary `stack().add().add().add().handler` chain -- `Middleware is assignable to`std/http``Handler` - meaning there is no +- `Middleware` is assignable to `std/http` `Handler` - meaning there is no additional wrapping necessary - Middleware composition is completely type safe and order-aware. This means that all requirements that are present but not fulfilled by previous - middleware "bubbles up" and will type error when trying to register it on the + middleware "bubble up" and will type error when trying to register it on the `Server`, stating which properties are missing - To be fair, it makes some assumptions. It assumes that you always add the same - type to your `next` call, so if you have conditional calls, you need to - "flatten" the types. It also assumes that you do not throw away the previous - request context. However, I think those are reasonable assumptions and they - are also present (and a lot less safe) in the current TS middleware concepts - e.g. in koa / oak. +To be fair, it makes some assumptions. It assumes that you always add the same +type to your `next` call, so if you have conditional `next` calls with different types, you need to +"flatten" the types. It also assumes that you do not throw away the previous +request context. However, I think those are reasonable assumptions and they +are also present (and a lot less safe) in other current TS middleware concepts +e.g. in koa / oak. ### Play around with it @@ -141,15 +141,38 @@ below. **The implemented middleware is just for presentation purposes**, it's implementation is very bad, but it works to show the idea. 1. Check out the branch, e.g. with - `$ git remote add lionc git@github.com:LionC/deno_std.git && git fetch && git switch middleware-experiment` -2. Start the server with `$ deno run --allow-net http/middleware/poc/server.ts` -3. Throw some requests at it, here are some `httpie` example commands: -- Succeed (without any animals): `http --json 0.0.0.0:5000/ name=My entryFee:=10 animals:='[{"name": "Kim", "kind": "Tiger"}, {"name": "Flippo", "kind": "Hippo"}, {"name": "Jasmin", "kind": "Tiger"}]'` -- Fail validation: `$ http --json 0.0.0.0:5000/ name=My entryFee:=10` -- Fail JSON content type: `$ http --form 0.0.0.0:5000/ name=My entryFee:=10` + ```sh + git remote add lionc git@github.com:LionC/deno_std.git + git fetch + git switch middleware-experiment + ``` +2. Start the server with -`http/middleware/poc/server.ts` is also a good place to play around with the type safe composition - try changing the order of middleware, leave a vital one out and see how LSP / tsc react. + ```sh + deno run --allow-net http/middleware/poc/server.ts + ``` +Now you can throw some requests at it, here are some `httpie` example commands: + +- Succeed + + ```sh + http --json 0.0.0.0:5000/ name=My entryFee:=10 animals:='[{"name": "Kim", "kind": "Tiger"}, {"name": "Flippo", "kind": "Hippo"}, {"name": "Jasmin", "kind": "Tiger"}]' + ``` +- Fail validation: + + ```sh + $ http --json 0.0.0.0:5000/ name=My entryFee:=10 + ``` +- Fail JSON content type: + + ```sh + $ http --form 0.0.0.0:5000/ name=My entryFee:=10 + ``` + +`http/middleware/poc/server.ts` is also a good place to play around with the +type safe composition - try changing the order of middleware, leave a vital one +out and see how LSP / tsc react. ## What now? From f0384412875c785aea2cdb4b4662fa098f39d3db Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 15:10:12 +0200 Subject: [PATCH 16/84] :memo: --- http/middleware/discussion.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/http/middleware/discussion.md b/http/middleware/discussion.md index 7ed477ba0dd4..67598bb5a8a6 100644 --- a/http/middleware/discussion.md +++ b/http/middleware/discussion.md @@ -144,14 +144,14 @@ implementation is very bad, but it works to show the idea. ```sh git remote add lionc git@github.com:LionC/deno_std.git - git fetch + git fetch lionc git switch middleware-experiment ``` 2. Start the server with ```sh - deno run --allow-net http/middleware/poc/server.ts - ``` + deno run --allow-net http/middleware/poc/server.ts + ``` Now you can throw some requests at it, here are some `httpie` example commands: - Succeed From 98333f7682cd55d18936f9b2168b4d4c9ed4e914 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Mon, 20 Sep 2021 15:14:04 +0200 Subject: [PATCH 17/84] :construction: --- http/playground.ts | 42 ------------------------------------------ http/router.ts | 17 ----------------- 2 files changed, 59 deletions(-) delete mode 100644 http/playground.ts delete mode 100644 http/router.ts diff --git a/http/playground.ts b/http/playground.ts deleted file mode 100644 index a48d986b361c..000000000000 --- a/http/playground.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { addMiddleware, Middleware, stack } from "./middleware.ts"; -import { Handler } from "./mod.ts"; - -type AuthedRequest = Request & { auth: string }; - -const passThrough: Middleware = (req, con, next) => next!(req, con); - -const authenticate: Middleware = async ( - req, - con, - next, -) => { - const auth = req.headers.get("authorization"); - - if (auth === null) { - return new Response(null, { status: 401 }); - } - - return await next!({ ...req, auth }, con); -}; - -const authorize: Middleware = async (req, con, next) => { - const { auth } = req; - - if (auth !== "asdf") { - return new Response(null, { status: 401 }); - } - - return await next!(req, con); -}; - -const handle: Middleware = async (req: AuthedRequest) => { - return new Response(`Hi ${req.auth}`, { status: 200 }); -}; - -const test = stack(passThrough) - .add(authenticate) - .add(authorize) - .add(handle) - .handler; - -const http: Handler = test; diff --git a/http/router.ts b/http/router.ts deleted file mode 100644 index ed642311c54f..000000000000 --- a/http/router.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Handler } from "./mod.ts"; - -export function router( - routes: Map<[RegExp, Request["method"]], Handler>, -): Handler { - return async (req, con) => { - const { pathname: path } = new URL(req.url); - - for (const [[pattern, method], handler] of routes) { - if (req.method === method && pattern.exec(path) !== null) { - return await handler(req, con); - } - } - - return new Response(null, { status: 404, statusText: "Route not found" }); - }; -} From ec8eeed071c14a88e2326282b005061bb2e35af1 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 22 Sep 2021 22:08:44 +0200 Subject: [PATCH 18/84] :recycle: Wrap Request and refactor accordingly --- http/middleware.ts | 31 +++-- http/middleware/discussion.md | 42 ++++--- http/middleware/json.ts | 10 +- http/middleware/log.ts | 4 +- http/middleware/poc/server.ts | 14 ++- http/middleware/poc/validate_zoo.ts | 27 ++--- http/request.ts | 180 ++++++++++++++++++++++++++++ http/server.ts | 12 +- 8 files changed, 246 insertions(+), 74 deletions(-) create mode 100644 http/request.ts diff --git a/http/middleware.ts b/http/middleware.ts index a07689c66e04..72f157d7f181 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,23 +1,22 @@ -import { ConnInfo } from "./server.ts"; +import { HttpRequest } from "./request.ts"; export type Middleware< - Requires extends Request, + Requires extends {} = {}, // deno-lint-ignore ban-types Adds = {}, -> = ( +> = >( req: Gets, - con: ConnInfo, - next?: Middleware, + next?: Middleware, ) => Promise; type MiddlewareStack< - Requires extends Request, + Requires extends {}, // deno-lint-ignore ban-types Adds = {}, > = { handler: Middleware; - add( + add( middleware: Middleware, ): MiddlewareStack< Requires & Omit, @@ -25,11 +24,11 @@ type MiddlewareStack< >; }; -export function addMiddleware< - FirstRequires extends Request, - FirstAdd, - SecondRequires extends Request, - SecondAdd, +export function composeMiddleware< + FirstRequires extends {}, + FirstAdd extends {}, + SecondRequires extends {}, + SecondAdd extends {}, >( first: Middleware, second: Middleware, @@ -37,29 +36,27 @@ export function addMiddleware< FirstRequires & Omit, FirstAdd & SecondAdd > { - return (req, con, next) => + return (req, next) => first( req, - con, (r) => second( //@ts-ignore: TS does not know about the middleware magic here r, - con, next, ), ); } // deno-lint-ignore ban-types -export function stack( +export function stack( middleware: Middleware, ): MiddlewareStack { return { handler: middleware, add: (m) => stack( - addMiddleware( + composeMiddleware( middleware, m, ), diff --git a/http/middleware/discussion.md b/http/middleware/discussion.md index 67598bb5a8a6..6b4d18daf78a 100644 --- a/http/middleware/discussion.md +++ b/http/middleware/discussion.md @@ -26,17 +26,16 @@ ## POC -[Here is a branch](https://github.com/LionC/deno_std/tree/middleware-experiment/http) in -which I have built a small dirty POC fullfiling the goals above. **This is just -to show the idea**. It is not fleshed out, very rough around a lot of edges, -has subpar ergonomics and several straight up bugs. All of them are solvable in -several ways and their solution is not vital to the concept, so I left them as -they are for the sake of starting a conversation. - -I stopped writing as soon as I was sure enough that this can be done -reasonably. There are many ways to do this basic concept and a lot of them are -viable - I did not want to invest into one of them, just have something to -start talking. +[Here is a branch](https://github.com/LionC/deno_std/tree/middleware-experiment/http) +in which I have built a small dirty POC fullfiling the goals above. **This is +just to show the idea**. It is not fleshed out, very rough around a lot of +edges, has subpar ergonomics and several straight up bugs. All of them are +solvable in several ways and their solution is not vital to the concept, so I +left them as they are for the sake of starting a conversation. + +I stopped writing as soon as I was sure enough that this can be done reasonably. +There are many ways to do this basic concept and a lot of them are viable - I +did not want to invest into one of them, just have something to start talking. ### API @@ -55,7 +54,11 @@ The components are: functions in here to show the idea): ```typescript - const validateFoo: Middleware = async (req, con, next) => { + const validateFoo: Middleware = async ( + req, + con, + next, + ) => { const body = extractBody(req); if (!isFoo(body)) { @@ -120,19 +123,19 @@ The components above fulfill the goals mentioned above: - `Middleware` is just a function, including the result of an arbitrary `stack().add().add().add().handler` chain -- `Middleware` is assignable to `std/http` `Handler` - meaning there is no - additional wrapping necessary +- `Middleware` is assignable to `std/http` `Handler` - meaning there is + no additional wrapping necessary - Middleware composition is completely type safe and order-aware. This means that all requirements that are present but not fulfilled by previous middleware "bubble up" and will type error when trying to register it on the `Server`, stating which properties are missing To be fair, it makes some assumptions. It assumes that you always add the same -type to your `next` call, so if you have conditional `next` calls with different types, you need to -"flatten" the types. It also assumes that you do not throw away the previous -request context. However, I think those are reasonable assumptions and they -are also present (and a lot less safe) in other current TS middleware concepts -e.g. in koa / oak. +type to your `next` call, so if you have conditional `next` calls with different +types, you need to "flatten" the types. It also assumes that you do not throw +away the previous request context. However, I think those are reasonable +assumptions and they are also present (and a lot less safe) in other current TS +middleware concepts e.g. in koa / oak. ### Play around with it @@ -152,6 +155,7 @@ implementation is very bad, but it works to show the idea. ```sh deno run --allow-net http/middleware/poc/server.ts ``` + Now you can throw some requests at it, here are some `httpie` example commands: - Succeed diff --git a/http/middleware/json.ts b/http/middleware/json.ts index 4493948c8bdc..0f92ab130149 100644 --- a/http/middleware/json.ts +++ b/http/middleware/json.ts @@ -1,8 +1,7 @@ import { Middleware } from "../middleware.ts"; -export const acceptJson: Middleware = async ( +export const acceptJson: Middleware<{}, { parsedBody: unknown }> = async ( req, - con, next, ) => { if (!req.headers.get("content-type")?.includes("application/json")) { @@ -29,10 +28,7 @@ export const acceptJson: Middleware = async ( throw e; } - const nextReq = { - ...req, - parsedBody, - }; + const nextReq = req.addContext({ parsedBody }); - return next!(nextReq, con); + return next!(nextReq); }; diff --git a/http/middleware/log.ts b/http/middleware/log.ts index 89eda95fbd51..7577f46fecb3 100644 --- a/http/middleware/log.ts +++ b/http/middleware/log.ts @@ -1,8 +1,8 @@ import { Middleware } from "../middleware.ts"; -export const log: Middleware = async (req, con, next) => { +export const log: Middleware = async (req, next) => { const start = performance.now(); - const response = await next!(req, con); + const response = await next!(req); const end = performance.now(); console.log( diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index fffe33c776e7..1d37b5f4f3b6 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -3,11 +3,12 @@ import { listenAndServe } from "../../server.ts"; import { acceptJson } from "../json.ts"; import { log } from "../log.ts"; import { Middleware, stack } from "../../middleware.ts"; +import { HttpRequest } from "../../request.ts"; import { validateZoo } from "./validate_zoo.ts"; import { Zoo } from "./zoo.ts"; -const createZoo: Middleware = (req) => { - const { zoo } = req; +async function createZoo(req: HttpRequest<{ zoo: Zoo }>) { + const { zoo } = req.context; const responseMessage = ` Your nice ${zoo.name} Zoo was created. @@ -20,12 +21,15 @@ All those ${ `; return Promise.resolve(new Response(responseMessage, { status: 201 })); -}; +} const handleCreateZoo = stack(log) .add(acceptJson) .add(validateZoo) - .add(createZoo) + .add<{ zoo: Zoo }>(createZoo) .handler; -await listenAndServe("0.0.0.0:5000", handleCreateZoo); +await listenAndServe( + "0.0.0.0:5000", + handleCreateZoo, +); diff --git a/http/middleware/poc/validate_zoo.ts b/http/middleware/poc/validate_zoo.ts index aaff9102ddf1..d08e9a1f0c2e 100644 --- a/http/middleware/poc/validate_zoo.ts +++ b/http/middleware/poc/validate_zoo.ts @@ -24,20 +24,15 @@ function isAnimalKind(subject: unknown): subject is AnimalKind { return typeof subject === "string" && includesValue(AnimalKind, subject); } -export const validateZoo: Middleware< - Request & { parsedBody: unknown }, - { zoo: Zoo } -> = async (req, con, next) => { - const { parsedBody } = req; - - if (!isZoo(parsedBody)) { - return new Response("Invalid Zoo", { status: 422 }); - } - - const nextReq = { - ...req, - zoo: parsedBody, - }; +export const validateZoo: Middleware<{ parsedBody: unknown }, { zoo: Zoo }> = + async (req, next) => { + const { parsedBody } = req.context; + + if (!isZoo(parsedBody)) { + return new Response("Invalid Zoo", { status: 422 }); + } - return await next!(nextReq, con); -}; + const nextReq = req.addContext({ zoo: parsedBody }); + + return await next!(nextReq); + }; diff --git a/http/request.ts b/http/request.ts new file mode 100644 index 000000000000..d28b7b5091fb --- /dev/null +++ b/http/request.ts @@ -0,0 +1,180 @@ +import { ConnInfo } from "./server.ts"; + +export class HttpRequest implements Request { + #context: C; + + constructor( + private request: Request, + readonly connInfo: ConnInfo, + context: C, + ) { + this.#context = context; + } + + get context(): C { + return this.#context; + } + + addContext(contextToAdd: N): HttpRequest { + this.#context = { ...this.#context, ...contextToAdd }; + + //@ts-ignore Limitations of mutation and types, but we should mutate for performance + return this as HttpRequest; + } + /** + * Returns the cache mode associated with request, which is a string + * indicating how the request will interact with the browser's cache when + * fetching. + */ + get cache(): RequestCache { + return this.request.cache; + } + /** + * Returns the credentials mode associated with request, which is a string + * indicating whether credentials will be sent with the request always, never, + * or only when sent to a same-origin URL. + */ + get credentials(): RequestCredentials { + return this.request.credentials; + } + /** + * Returns the kind of resource requested by request, e.g., "document" or "script". + */ + get destination(): RequestDestination { + return this.request.destination; + } + /** + * Returns a Headers object consisting of the headers associated with request. + * Note that headers added in the network layer by the user agent will not be + * accounted for in this object, e.g., the "Host" header. + */ + get headers(): Headers { + return this.request.headers; + } + /** + * Returns request's subresource integrity metadata, which is a cryptographic + * hash of the resource being fetched. Its value consists of multiple hashes + * separated by whitespace. [SRI] + */ + get integrity(): string { + return this.request.integrity; + } + /** + * Returns a boolean indicating whether or not request is for a history + * navigation (a.k.a. back-forward navigation). + */ + get isHistoryNavigation(): boolean { + return this.request.isHistoryNavigation; + } + /** + * Returns a boolean indicating whether or not request is for a reload + * navigation. + */ + get isReloadNavigation(): boolean { + return this.request.isReloadNavigation; + } + /** + * Returns a boolean indicating whether or not request can outlive the global + * in which it was created. + */ + get keepalive(): boolean { + return this.request.keepalive; + } + /** + * Returns request's HTTP method, which is "GET" by default. + */ + get method(): string { + return this.request.method; + } + /** + * Returns the mode associated with request, which is a string indicating + * whether the request will use CORS, or will be restricted to same-origin + * URLs. + */ + get mode(): RequestMode { + return this.request.mode; + } + /** + * Returns the redirect mode associated with request, which is a string + * indicating how redirects for the request will be handled during fetching. A + * request will follow redirects by default. + */ + get redirect(): RequestRedirect { + return this.request.redirect; + } + /** + * Returns the referrer of request. Its value can be a same-origin URL if + * explicitly set in init, the empty string to indicate no referrer, and + * "about:client" when defaulting to the global's default. This is used during + * fetching to determine the value of the `Referer` header of the request + * being made. + */ + get referrer(): string { + return this.request.referrer; + } + /** + * Returns the referrer policy associated with request. This is used during + * fetching to compute the value of the request's referrer. + */ + get referrerPolicy(): ReferrerPolicy { + return this.request.referrerPolicy; + } + /** + * Returns the signal associated with request, which is an AbortSignal object + * indicating whether or not request has been aborted, and its abort event + * handler. + */ + get signal(): AbortSignal { + return this.request.signal; + } + /** + * Returns the URL of request as a string. + */ + get url(): string { + return this.request.url; + } + + /** A simple getter used to expose a `ReadableStream` of the body contents. */ + get body(): ReadableStream | null { + return this.request.body; + } + /** Stores a `Boolean` that declares whether the body has been used in a + * response yet. + */ + get bodyUsed(): boolean { + return this.request.bodyUsed; + } + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with an `ArrayBuffer`. + */ + arrayBuffer(): Promise { + return this.request.arrayBuffer(); + } + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with a `Blob`. + */ + blob(): Promise { + return this.request.blob(); + } + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with a `FormData` object. + */ + formData(): Promise { + return this.request.formData(); + } + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with the result of parsing the body text as JSON. + */ + json(): Promise { + return this.request.json(); + } + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with a `USVString` (text). + */ + text(): Promise { + return this.request.text(); + } + clone(): Request { + return this.request.clone(); + } +} diff --git a/http/server.ts b/http/server.ts index ea123b012c45..cb570bce4111 100644 --- a/http/server.ts +++ b/http/server.ts @@ -1,5 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. import { delay } from "../async/mod.ts"; +import { HttpRequest } from "./request.ts"; /** Thrown by Server after it has been closed. */ const ERROR_SERVER_CLOSED = "Server closed"; @@ -35,10 +36,7 @@ export interface ConnInfo { * of the error is isolated to the individual request. It will catch the error * and close the underlying connection. */ -export type Handler = ( - request: Request, - connInfo: ConnInfo, -) => Response | Promise; +export type Handler = (request: HttpRequest) => Response | Promise; /** * Parse an address from a string. @@ -346,11 +344,9 @@ export class Server { connInfo: ConnInfo, ): Promise { try { + const req = new HttpRequest(requestEvent.request, connInfo, {}); // Handle the request event, generating a response. - const response = await this.#handler( - requestEvent.request, - connInfo, - ); + const response = await this.#handler(req); // Send the response. await requestEvent.respondWith(response); From 03d8685de5ee84aba9a7e298064a66b9db47cc3b Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 23 Sep 2021 18:07:22 +0200 Subject: [PATCH 19/84] :label: :recycle: Add more typing smarts for a better DX --- _util/types.ts | 16 +++++++++++ http/middleware.ts | 52 ++++++++++++++++++++--------------- http/middleware/poc/server.ts | 4 +-- http/request.ts | 3 +- 4 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 _util/types.ts diff --git a/_util/types.ts b/_util/types.ts new file mode 100644 index 000000000000..347bd386e767 --- /dev/null +++ b/_util/types.ts @@ -0,0 +1,16 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +export type Expand = T extends Record + ? T extends infer O + ? { [K in keyof O]: O[K] } + : never + : T; + +export type SafeOmit = Omit> + +export type CommonKeys = keyof A & keyof B + +export type CommonKeysWithSameType = { + [K in CommonKeys]: T[K] extends U[K] ? K : never +}[CommonKeys] + diff --git a/http/middleware.ts b/http/middleware.ts index 72f157d7f181..f9f147f65305 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,43 +1,52 @@ +// deno-lint-ignore-file ban-types + import { HttpRequest } from "./request.ts"; +import { Expand, SafeOmit } from "../_util/types.ts"; export type Middleware< - Requires extends {} = {}, - // deno-lint-ignore ban-types + Needs extends {} = {}, Adds = {}, -> = >( - req: Gets, - next?: Middleware, +> = ( + req: HttpRequest, + next?: Middleware>, ) => Promise; type MiddlewareStack< - Requires extends {}, - // deno-lint-ignore ban-types + Needs extends {}, Adds = {}, > = { - handler: Middleware; + handler: Middleware; - add( - middleware: Middleware, + // add( + // handler: (req: HttpRequest) => Promise + // ): TerminatedMiddlewareStack >; + add( + middleware: Middleware, ): MiddlewareStack< - Requires & Omit, - Adds & AddedAdds + Expand>, + Expand >; }; +type TerminatedMiddlewareStack = { + handler: Middleware; +}; + export function composeMiddleware< - FirstRequires extends {}, + FirstNeeds extends {}, FirstAdd extends {}, - SecondRequires extends {}, + SecondNeeds extends {}, SecondAdd extends {}, >( - first: Middleware, - second: Middleware, + first: Middleware, + second: Middleware, ): Middleware< - FirstRequires & Omit, - FirstAdd & SecondAdd + Expand>, + Expand > { return (req, next) => first( + //@ts-ignore asdf req, (r) => second( @@ -48,10 +57,9 @@ export function composeMiddleware< ); } -// deno-lint-ignore ban-types -export function stack( - middleware: Middleware, -): MiddlewareStack { +export function stack( + middleware: Middleware, +): MiddlewareStack { return { handler: middleware, add: (m) => diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index 1d37b5f4f3b6..0831fa796b16 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -7,7 +7,7 @@ import { HttpRequest } from "../../request.ts"; import { validateZoo } from "./validate_zoo.ts"; import { Zoo } from "./zoo.ts"; -async function createZoo(req: HttpRequest<{ zoo: Zoo }>) { +function createZoo(req: HttpRequest<{ zoo: Zoo }>) { const { zoo } = req.context; const responseMessage = ` Your nice ${zoo.name} Zoo was created. @@ -26,7 +26,7 @@ All those ${ const handleCreateZoo = stack(log) .add(acceptJson) .add(validateZoo) - .add<{ zoo: Zoo }>(createZoo) + .add<{ zoo: Zoo }, {}>(createZoo) .handler; await listenAndServe( diff --git a/http/request.ts b/http/request.ts index d28b7b5091fb..7f1b2b1ac0d2 100644 --- a/http/request.ts +++ b/http/request.ts @@ -1,4 +1,5 @@ import { ConnInfo } from "./server.ts"; +import { Expand } from "../_util/types.ts"; export class HttpRequest implements Request { #context: C; @@ -15,7 +16,7 @@ export class HttpRequest implements Request { return this.#context; } - addContext(contextToAdd: N): HttpRequest { + addContext(contextToAdd: N): HttpRequest> { this.#context = { ...this.#context, ...contextToAdd }; //@ts-ignore Limitations of mutation and types, but we should mutate for performance From 8d5577cf1665563fbeb63bc476c80b2a698465b2 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sat, 25 Sep 2021 10:20:49 +0200 Subject: [PATCH 20/84] :fire: Remove stakadd overload and adapt typees for DX --- http/middleware.ts | 7 ------- http/middleware/poc/server.ts | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index f9f147f65305..3fa8f3176189 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -17,9 +17,6 @@ type MiddlewareStack< > = { handler: Middleware; - // add( - // handler: (req: HttpRequest) => Promise - // ): TerminatedMiddlewareStack >; add( middleware: Middleware, ): MiddlewareStack< @@ -28,10 +25,6 @@ type MiddlewareStack< >; }; -type TerminatedMiddlewareStack = { - handler: Middleware; -}; - export function composeMiddleware< FirstNeeds extends {}, FirstAdd extends {}, diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index 0831fa796b16..54ffc70acb7f 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -2,7 +2,7 @@ import { distinctBy } from "../../../collections/mod.ts"; import { listenAndServe } from "../../server.ts"; import { acceptJson } from "../json.ts"; import { log } from "../log.ts"; -import { Middleware, stack } from "../../middleware.ts"; +import { stack } from "../../middleware.ts"; import { HttpRequest } from "../../request.ts"; import { validateZoo } from "./validate_zoo.ts"; import { Zoo } from "./zoo.ts"; @@ -10,23 +10,31 @@ import { Zoo } from "./zoo.ts"; function createZoo(req: HttpRequest<{ zoo: Zoo }>) { const { zoo } = req.context; const responseMessage = ` -Your nice ${zoo.name} Zoo was created. - -Take good care of your ${zoo.animals.length} animals! -All those ${ + Your nice ${zoo.name} Zoo was created. + + Take good care of your ${zoo.animals.length} animals! + All those ${ distinctBy(zoo.animals, (it) => it.kind).map((it) => `${it.kind}s`).join( " and ", ) } will surely amaze your visitors. -`; + `; return Promise.resolve(new Response(responseMessage, { status: 201 })); } +function passThrough( + req: HttpRequest, + next?: (r: HttpRequest) => Promise, +) { + return next!(req); +} + const handleCreateZoo = stack(log) + .add(passThrough) .add(acceptJson) .add(validateZoo) - .add<{ zoo: Zoo }, {}>(createZoo) + .add(createZoo) .handler; await listenAndServe( From 19227439514f57ec0782fe9b503fb1ec1b48f262 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sat, 25 Sep 2021 21:51:15 +0200 Subject: [PATCH 21/84] :sparkles: Remove the need to call .handler for middleware chains and rename them --- http/middleware.ts | 24 +++++++++++++----------- http/middleware/poc/server.ts | 5 ++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index 3fa8f3176189..b318b8f192fa 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -11,15 +11,15 @@ export type Middleware< next?: Middleware>, ) => Promise; -type MiddlewareStack< +type MiddlewareChain< Needs extends {}, Adds = {}, > = { - handler: Middleware; + (...args: Parameters>): ReturnType> add( middleware: Middleware, - ): MiddlewareStack< + ): MiddlewareChain< Expand>, Expand >; @@ -50,17 +50,19 @@ export function composeMiddleware< ); } -export function stack( +export function chain( middleware: Middleware, -): MiddlewareStack { - return { - handler: middleware, - add: (m) => - stack( +): MiddlewareChain { + const copy = middleware.bind({}) as MiddlewareChain + + copy.add = (m) => + chain( composeMiddleware( middleware, m, ), - ), - }; + ) + + return copy } + diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index 54ffc70acb7f..734fa0a6f6ff 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -2,7 +2,7 @@ import { distinctBy } from "../../../collections/mod.ts"; import { listenAndServe } from "../../server.ts"; import { acceptJson } from "../json.ts"; import { log } from "../log.ts"; -import { stack } from "../../middleware.ts"; +import { chain } from "../../middleware.ts"; import { HttpRequest } from "../../request.ts"; import { validateZoo } from "./validate_zoo.ts"; import { Zoo } from "./zoo.ts"; @@ -30,12 +30,11 @@ function passThrough( return next!(req); } -const handleCreateZoo = stack(log) +const handleCreateZoo = chain(log) .add(passThrough) .add(acceptJson) .add(validateZoo) .add(createZoo) - .handler; await listenAndServe( "0.0.0.0:5000", From d890772215a20355a846f0476877b54e6d7cfeeb Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 16:26:54 +0200 Subject: [PATCH 22/84] :sparkles: :fire: Add parsedUrl to HttpRequest and remove redundant JSDoc --- http/request.ts | 117 +++++++++++++----------------------------------- 1 file changed, 31 insertions(+), 86 deletions(-) diff --git a/http/request.ts b/http/request.ts index 7f1b2b1ac0d2..2bb6d0a48d4b 100644 --- a/http/request.ts +++ b/http/request.ts @@ -3,6 +3,7 @@ import { Expand } from "../_util/types.ts"; export class HttpRequest implements Request { #context: C; + #parsedUrl?: URL = undefined; constructor( private request: Request, @@ -22,159 +23,103 @@ export class HttpRequest implements Request { //@ts-ignore Limitations of mutation and types, but we should mutate for performance return this as HttpRequest; } - /** - * Returns the cache mode associated with request, which is a string - * indicating how the request will interact with the browser's cache when - * fetching. - */ + + get parsedUrl(): URL { + return this.#parsedUrl ?? + (this.#parsedUrl = new URL(this.url)); + } + /* ********************* + * * Request Delegates * + * ********************* */ + get cache(): RequestCache { return this.request.cache; } - /** - * Returns the credentials mode associated with request, which is a string - * indicating whether credentials will be sent with the request always, never, - * or only when sent to a same-origin URL. - */ + get credentials(): RequestCredentials { return this.request.credentials; } - /** - * Returns the kind of resource requested by request, e.g., "document" or "script". - */ + get destination(): RequestDestination { return this.request.destination; } - /** - * Returns a Headers object consisting of the headers associated with request. - * Note that headers added in the network layer by the user agent will not be - * accounted for in this object, e.g., the "Host" header. - */ + get headers(): Headers { return this.request.headers; } - /** - * Returns request's subresource integrity metadata, which is a cryptographic - * hash of the resource being fetched. Its value consists of multiple hashes - * separated by whitespace. [SRI] - */ + get integrity(): string { return this.request.integrity; } - /** - * Returns a boolean indicating whether or not request is for a history - * navigation (a.k.a. back-forward navigation). - */ + get isHistoryNavigation(): boolean { return this.request.isHistoryNavigation; } - /** - * Returns a boolean indicating whether or not request is for a reload - * navigation. - */ + get isReloadNavigation(): boolean { return this.request.isReloadNavigation; } - /** - * Returns a boolean indicating whether or not request can outlive the global - * in which it was created. - */ + get keepalive(): boolean { return this.request.keepalive; } - /** - * Returns request's HTTP method, which is "GET" by default. - */ + get method(): string { return this.request.method; } - /** - * Returns the mode associated with request, which is a string indicating - * whether the request will use CORS, or will be restricted to same-origin - * URLs. - */ + get mode(): RequestMode { return this.request.mode; } - /** - * Returns the redirect mode associated with request, which is a string - * indicating how redirects for the request will be handled during fetching. A - * request will follow redirects by default. - */ + get redirect(): RequestRedirect { return this.request.redirect; } - /** - * Returns the referrer of request. Its value can be a same-origin URL if - * explicitly set in init, the empty string to indicate no referrer, and - * "about:client" when defaulting to the global's default. This is used during - * fetching to determine the value of the `Referer` header of the request - * being made. - */ + get referrer(): string { return this.request.referrer; } - /** - * Returns the referrer policy associated with request. This is used during - * fetching to compute the value of the request's referrer. - */ + get referrerPolicy(): ReferrerPolicy { return this.request.referrerPolicy; } - /** - * Returns the signal associated with request, which is an AbortSignal object - * indicating whether or not request has been aborted, and its abort event - * handler. - */ + get signal(): AbortSignal { return this.request.signal; } - /** - * Returns the URL of request as a string. - */ + get url(): string { return this.request.url; } - /** A simple getter used to expose a `ReadableStream` of the body contents. */ get body(): ReadableStream | null { return this.request.body; } - /** Stores a `Boolean` that declares whether the body has been used in a - * response yet. - */ + get bodyUsed(): boolean { return this.request.bodyUsed; } - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with an `ArrayBuffer`. - */ + arrayBuffer(): Promise { return this.request.arrayBuffer(); } - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with a `Blob`. - */ + blob(): Promise { return this.request.blob(); } - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with a `FormData` object. - */ + formData(): Promise { return this.request.formData(); } - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with the result of parsing the body text as JSON. - */ + json(): Promise { return this.request.json(); } - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with a `USVString` (text). - */ + text(): Promise { return this.request.text(); } + clone(): Request { return this.request.clone(); } From af545d91d5a7a70f71922f74c9aae0a0892e202b Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 19:52:37 +0200 Subject: [PATCH 23/84] :memo: Add JSDoc to HttpRequest --- http/request.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/http/request.ts b/http/request.ts index 2bb6d0a48d4b..47a777b209b0 100644 --- a/http/request.ts +++ b/http/request.ts @@ -1,10 +1,22 @@ import { ConnInfo } from "./server.ts"; import { Expand } from "../_util/types.ts"; +/** + * An incoming request. Follows the Request web standard, adding connection + * information, a mutable request context and some helpers for common use cases. + * + * @typeParam C - Type of the current request `context` */ export class HttpRequest implements Request { #context: C; #parsedUrl?: URL = undefined; + /** + * Wraps a request, adding connection information and request context to it. + * + * @param request The incoming `Request` to wrap + * @param connInfo Connection information about the connection `request` was received on + * @param context Initial request context, set to `{}` for empty context + */ constructor( private request: Request, readonly connInfo: ConnInfo, @@ -13,10 +25,27 @@ export class HttpRequest implements Request { this.#context = context; } + /** + * Current request context. Can be used to attach arbitrary request specific + * information e.g. in middleware. + */ get context(): C { return this.#context; } + /** + * Add information to the request context. The passed object will be merged + * with the current request context. + * + * Example: + * + * ```ts + * declare const req: HttpRequest + * + * const reqWithUser = req.addContext({ user: "Example" }) + * assertEquals(reqWithUser.context.user, "Example") + * ``` + */ addContext(contextToAdd: N): HttpRequest> { this.#context = { ...this.#context, ...contextToAdd }; @@ -24,6 +53,7 @@ export class HttpRequest implements Request { return this as HttpRequest; } + /** `URL` object representation of this request's url */ get parsedUrl(): URL { return this.#parsedUrl ?? (this.#parsedUrl = new URL(this.url)); From bf36fe07fbf13067e0f932d052a65aea4cab1a9c Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 19:53:22 +0200 Subject: [PATCH 24/84] :sparkles: Make HttpRequest.clone re-wrap the cloned request --- http/request.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/http/request.ts b/http/request.ts index 47a777b209b0..d10104d62be9 100644 --- a/http/request.ts +++ b/http/request.ts @@ -151,6 +151,10 @@ export class HttpRequest implements Request { } clone(): Request { - return this.request.clone(); + return new HttpRequest( + this.request.clone(), + this.connInfo, + this.context, + ); } } From 8666c6298e14bd1974b2e9a0f457c42a1dfee5d2 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 20:04:38 +0200 Subject: [PATCH 25/84] :fire: :label: Remove redundant type annotations on Request delegates in HttpRequest --- http/request.ts | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/http/request.ts b/http/request.ts index d10104d62be9..256126809649 100644 --- a/http/request.ts +++ b/http/request.ts @@ -62,95 +62,95 @@ export class HttpRequest implements Request { * * Request Delegates * * ********************* */ - get cache(): RequestCache { + get cache() { return this.request.cache; } - get credentials(): RequestCredentials { + get credentials() { return this.request.credentials; } - get destination(): RequestDestination { + get destination() { return this.request.destination; } - get headers(): Headers { + get headers() { return this.request.headers; } - get integrity(): string { + get integrity() { return this.request.integrity; } - get isHistoryNavigation(): boolean { + get isHistoryNavigation() { return this.request.isHistoryNavigation; } - get isReloadNavigation(): boolean { + get isReloadNavigation() { return this.request.isReloadNavigation; } - get keepalive(): boolean { + get keepalive() { return this.request.keepalive; } - get method(): string { + get method() { return this.request.method; } - get mode(): RequestMode { + get mode() { return this.request.mode; } - get redirect(): RequestRedirect { + get redirect() { return this.request.redirect; } - get referrer(): string { + get referrer() { return this.request.referrer; } - get referrerPolicy(): ReferrerPolicy { + get referrerPolicy() { return this.request.referrerPolicy; } - get signal(): AbortSignal { + get signal() { return this.request.signal; } - get url(): string { + get url() { return this.request.url; } - get body(): ReadableStream | null { + get body() { return this.request.body; } - get bodyUsed(): boolean { + get bodyUsed() { return this.request.bodyUsed; } - arrayBuffer(): Promise { + arrayBuffer() { return this.request.arrayBuffer(); } - blob(): Promise { + blob() { return this.request.blob(); } - formData(): Promise { + formData() { return this.request.formData(); } - json(): Promise { + json() { return this.request.json(); } - text(): Promise { + text() { return this.request.text(); } - clone(): Request { + clone() { return new HttpRequest( this.request.clone(), this.connInfo, From 26f128c02d0521285d1b0f1a202917377b315cec Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 20:08:30 +0200 Subject: [PATCH 26/84] :memo: Improve HttpRequest context and addContetxt JSDoc --- http/request.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/http/request.ts b/http/request.ts index 256126809649..e1c298902fad 100644 --- a/http/request.ts +++ b/http/request.ts @@ -26,15 +26,15 @@ export class HttpRequest implements Request { } /** - * Current request context. Can be used to attach arbitrary request specific - * information e.g. in middleware. + * Contains arbitrary custom request specific context data. Can be set during + * construction or via `addContext` */ get context(): C { return this.#context; } /** - * Add information to the request context. The passed object will be merged + * Add information to the request `context`. The passed object will be merged * with the current request context. * * Example: From cd33d757959cb9fb2539b3e8f232a7160eac9987 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 20:26:44 +0200 Subject: [PATCH 27/84] :memo: Make examples consistent and use mod imports --- http/mod.ts | 1 + http/server.ts | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/http/mod.ts b/http/mod.ts index 827eaebf80fa..1eff0e5be978 100644 --- a/http/mod.ts +++ b/http/mod.ts @@ -2,3 +2,4 @@ export * from "./cookie.ts"; export * from "./http_status.ts"; export * from "./server.ts"; +export * from "./request.ts"; diff --git a/http/server.ts b/http/server.ts index cb570bce4111..e68c512348b0 100644 --- a/http/server.ts +++ b/http/server.ts @@ -115,10 +115,10 @@ export class Server { * Constructs a new HTTP Server instance. * * ```ts - * import { Server } from "https://deno.land/std@$STD_VERSION/http/server.ts"; + * import { Server, HttpRequest } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * const addr = ":4505"; - * const handler = (request: Request) => { + * const handler = (request: HttpRequest) => { * const body = `Your user-agent is:\n\n${request.headers.get( * "user-agent", * ) ?? "Unknown"}`; @@ -148,9 +148,9 @@ export class Server { * Will always close the created listener. * * ```ts - * import { Server } from "https://deno.land/std@$STD_VERSION/http/server.ts"; + * import { Server, HttpRequest } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * - * const handler = (request: Request) => { + * const handler = (request: HttpRequest) => { * const body = `Your user-agent is:\n\n${request.headers.get( * "user-agent", * ) ?? "Unknown"}`; @@ -201,10 +201,10 @@ export class Server { * Throws a server closed error if the server has been closed. * * ```ts - * import { Server } from "https://deno.land/std@$STD_VERSION/http/server.ts"; + * import { Server, HttpRequest } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * const addr = ":4505"; - * const handler = (request: Request) => { + * const handler = (request: HttpRequest) => { * const body = `Your user-agent is:\n\n${request.headers.get( * "user-agent", * ) ?? "Unknown"}`; @@ -248,10 +248,10 @@ export class Server { * Throws a server closed error if the server has been closed. * * ```ts - * import { Server } from "https://deno.land/std@$STD_VERSION/http/server.ts"; + * import { Server, HttpRequest } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * const addr = ":4505"; - * const handler = (request: Request) => { + * const handler = (request: HttpRequest) => { * const body = `Your user-agent is:\n\n${request.headers.get( * "user-agent", * ) ?? "Unknown"}`; @@ -345,7 +345,7 @@ export class Server { ): Promise { try { const req = new HttpRequest(requestEvent.request, connInfo, {}); - // Handle the request event, generating a response. + // Handle the request, generating a response. const response = await this.#handler(req); // Send the response. @@ -533,7 +533,7 @@ export interface ServeInit { * handles requests on these connections with the given handler. * * ```ts - * import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts"; + * import { serve } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * const listener = Deno.listen({ port: 4505 }); * @@ -577,7 +577,7 @@ export async function serve( * `0.0.0.0` is used. * * ```ts - * import { listenAndServe } from "https://deno.land/std@$STD_VERSION/http/server.ts"; + * import { listenAndServe } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * const addr = ":4505"; * @@ -621,7 +621,7 @@ export async function listenAndServe( * `0.0.0.0` is used. * * ```ts - * import { listenAndServeTls } from "https://deno.land/std@$STD_VERSION/http/server.ts"; + * import { listenAndServeTls } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * const addr = ":4505"; * const certFile = "/path/to/certFile.crt"; From 6598d223531ca7daf4adb3b5b9b2476b0728a2e9 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 22:11:57 +0200 Subject: [PATCH 28/84] :art: Format _util/types.ts --- _util/types.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/_util/types.ts b/_util/types.ts index 347bd386e767..01e3738006fd 100644 --- a/_util/types.ts +++ b/_util/types.ts @@ -1,16 +1,17 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. export type Expand = T extends Record - ? T extends infer O - ? { [K in keyof O]: O[K] } - : never + ? T extends infer O ? { [K in keyof O]: O[K] } + : never : T; -export type SafeOmit = Omit> +export type SafeOmit = Omit< + From, + CommonKeysWithSameType +>; -export type CommonKeys = keyof A & keyof B +export type CommonKeys = keyof A & keyof B; export type CommonKeysWithSameType = { - [K in CommonKeys]: T[K] extends U[K] ? K : never -}[CommonKeys] - + [K in CommonKeys]: T[K] extends U[K] ? K : never; +}[CommonKeys]; From 43b846d5532b303102e51e7725cf331653dfdd60 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 22:12:43 +0200 Subject: [PATCH 29/84] :memo: Add JSDoc to the middleware module --- http/middleware.ts | 102 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index b318b8f192fa..e2c057df9f65 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -3,6 +3,37 @@ import { HttpRequest } from "./request.ts"; import { Expand, SafeOmit } from "../_util/types.ts"; +/** +* Middleware Handler that is expected to call the `next` Middleware at some +* point, adding its own logic, request context and transformations befor or +* after it. Can express the request context (see `HttpRequest.context`) it +* requires to work and any request context extensions it will add to be +* available to the `next` middleware. +* +* @typeParam Needs - Request context required by this Middleware, defaults to +* empty context `{}` +* +* @typeParam Adds - Request context this Middleware will add and thus make +* available to the `next` Middleware, defaults to empty context `{}` +* +* Example: +* +* ```ts import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +* +* // Log response time, method and path for requests going through this middleware +* const log: Middleware = (req, next) => { +* const start = performance.now(); +* const response = await next!(req); +* const end = performance.now(); + +* console.log( +* `${req.method} ${new URL(req.url).pathname} ${response.status} ${end - start}ms` +* ); + +* return response; +* }; +* ``` +*/ export type Middleware< Needs extends {} = {}, Adds = {}, @@ -11,12 +42,42 @@ export type Middleware< next?: Middleware>, ) => Promise; -type MiddlewareChain< +/** + * A `Middleware` that can be chained onto with `.add()`. Use `chain()` to wrap + * a `Middleware` as a `MiddlewareChain`. */ +export type MiddlewareChain< Needs extends {}, Adds = {}, > = { - (...args: Parameters>): ReturnType> + ( + ...args: Parameters> + ): ReturnType>; + /** + * Chain a given middleware to this middleware, returning a new + * `MiddlewareChain`. + * + * Example: + * + * ```ts + * const first: Middleware = (req, next) => { + * console.log("Hey"); + * return await next!(req); + * }; + * + * const second: Middleware = (req, next) => { + * console.log("there!"); + * return await next!(req); + * }; + * + * const helloWorld = (req) => new Response("Hello world"); + * + * // This Handler will log "Hey", log "there!" and then respond with "Hello World" + * const handler = chain(first) + * .add(second) + * .add(helloWorld) + * ``` + */ add( middleware: Middleware, ): MiddlewareChain< @@ -25,6 +86,23 @@ type MiddlewareChain< >; }; +/** + * Builds a new `Middleware` out of two given ones, chaining them. + * + * Example: + * + * ```ts + * const findUser: Middleware<{}, { user: string }> = (req, next) => { + * return await next!( + * req.addContext({ user: "Kim" }); + * ); + * }; + * + * const hello = (req: HttpRequest<{ user: string }>) => new Response(`Hello ${req.context.user}`); + * + * // This Handler will respond with "Hello Kim" + * const handler = composeMiddleware(findUser, hello) + * ``` */ export function composeMiddleware< FirstNeeds extends {}, FirstAdd extends {}, @@ -50,19 +128,19 @@ export function composeMiddleware< ); } +/** Wraps the given middleware in a `MiddlewareChain` so it can be `.add()`ed onto */ export function chain( middleware: Middleware, ): MiddlewareChain { - const copy = middleware.bind({}) as MiddlewareChain + const copy = middleware.bind({}) as MiddlewareChain; - copy.add = (m) => - chain( - composeMiddleware( - middleware, - m, - ), - ) + copy.add = (m) => + chain( + composeMiddleware( + middleware, + m, + ), + ); - return copy + return copy; } - From f1245d4fd4954b1d01a30701372491b1e322fa63 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 26 Sep 2021 23:03:01 +0200 Subject: [PATCH 30/84] :art: Format --- http/middleware/poc/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index 734fa0a6f6ff..3a95677b33dd 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -34,7 +34,7 @@ const handleCreateZoo = chain(log) .add(passThrough) .add(acceptJson) .add(validateZoo) - .add(createZoo) + .add(createZoo); await listenAndServe( "0.0.0.0:5000", From ddc3ac8438358f92091903e19cb708214bccc8ec Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 14 Oct 2021 16:12:10 +0200 Subject: [PATCH 31/84] :construction: --- http/README.md | 12 ++++++++++++ http/middleware.ts | 2 +- http/middleware/json.ts | 1 + http/middleware/poc/server.ts | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/http/README.md b/http/README.md index 91f3c5c6247b..5b88fb25c646 100644 --- a/http/README.md +++ b/http/README.md @@ -1,3 +1,15 @@ +- Minimal server +- Handlers +- Middleware +- Remove Legacy Server + +# PR + +- Wrap Request + - Why + - Have single API + - Allows for cookies, comtext, lazy utilities… + # http `http` is a module to provide HTTP client and server implementations. diff --git a/http/middleware.ts b/http/middleware.ts index e2c057df9f65..9e7bed8c8260 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,7 +1,7 @@ // deno-lint-ignore-file ban-types import { HttpRequest } from "./request.ts"; -import { Expand, SafeOmit } from "../_util/types.ts"; +import type { Expand, SafeOmit } from "../_util/types.ts"; /** * Middleware Handler that is expected to call the `next` Middleware at some diff --git a/http/middleware/json.ts b/http/middleware/json.ts index 0f92ab130149..78df45b919fa 100644 --- a/http/middleware/json.ts +++ b/http/middleware/json.ts @@ -1,5 +1,6 @@ import { Middleware } from "../middleware.ts"; +// deno-lint-ignore ban-types export const acceptJson: Middleware<{}, { parsedBody: unknown }> = async ( req, next, diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts index 3a95677b33dd..0c038efa9489 100644 --- a/http/middleware/poc/server.ts +++ b/http/middleware/poc/server.ts @@ -5,7 +5,7 @@ import { log } from "../log.ts"; import { chain } from "../../middleware.ts"; import { HttpRequest } from "../../request.ts"; import { validateZoo } from "./validate_zoo.ts"; -import { Zoo } from "./zoo.ts"; +import type { Zoo } from "./zoo.ts"; function createZoo(req: HttpRequest<{ zoo: Zoo }>) { const { zoo } = req.context; From c043756ad7e23bf0f3efde31963356018dfe34f2 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 14 Oct 2021 19:46:39 +0200 Subject: [PATCH 32/84] :construction: :memo: Draft documentation --- http/README.md | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/http/README.md b/http/README.md index 5b88fb25c646..e0d5c849c74d 100644 --- a/http/README.md +++ b/http/README.md @@ -12,35 +12,32 @@ # http -`http` is a module to provide HTTP client and server implementations. +Deno's standard HTTP server based on the [blazing fast native http API](https://deno.land/manual/runtime/http_server_apis#http-server-apis) under the hood. -## Server +## Minimal Server -Server APIs utilizing Deno's -[HTTP server APIs](https://deno.land/manual/runtime/http_server_apis#http-server-apis). +Run this file with `--allow-net` and try requesting `http://localhost:8000`: ```ts -import { listenAndServe } from "https://deno.land/std@$STD_VERSION/http/server.ts"; +import { listenAndServe } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; -listenAndServe(":8000", () => new Response("Hello World\n")); - -console.log("http://localhost:8000/"); +await listenAndServe(":8000", () => new Response("Hello World")); ``` -## Legacy Server (Deprecated) +## Handling Requests -Legacy server APIs using a JavaScript HTTP server implementation. +`listenAndServe` expects a `Handler`, which is a function that receives an `HttpRequest` and returns a `Response`: -```ts -import { serve } from "https://deno.land/std@$STD_VERSION/http/server_legacy.ts"; +```typescript +export type Handler = (request: HttpRequest) => Response | Promise; +``` -const server = serve({ port: 8000 }); -console.log("http://localhost:8000/"); +`std/http` follows web standards, specifically parts of the Fetch API. `HttpRequest` is an extenstion of the +[`Request` web standard](TODO MDN LINK), adding connection information and some helper functions to make it +more convenient to use on servers. The expected return value follows the same standard and is expected to be +a [`Response`](TODO MDN LINK). -for await (const req of server) { - req.respond({ body: "Hello World\n" }); -} -``` +Here is an example handler that echoes the request body ## File Server From fac57294ec385deac055085b2fdd8bd16fecd3db Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 14 Oct 2021 19:48:05 +0200 Subject: [PATCH 33/84] :label: Improve middleware type ignores --- http/middleware.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index 9e7bed8c8260..4195a7ebe583 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -117,11 +117,10 @@ export function composeMiddleware< > { return (req, next) => first( - //@ts-ignore asdf - req, + req as HttpRequest, (r) => second( - //@ts-ignore: TS does not know about the middleware magic here + // @ts-ignore This will bubble up for insufficient middlware chains r, next, ), From 7129c33a1c3a8db4df1731456aabcf70fdf75e04 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 14 Oct 2021 19:48:41 +0200 Subject: [PATCH 34/84] :construction: :memo: Move legacy http docs to own file --- http/legacy_server.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 http/legacy_server.md diff --git a/http/legacy_server.md b/http/legacy_server.md new file mode 100644 index 000000000000..e69de29bb2d1 From 65dad7b50f79059361d4402fadf32d68cfd5ca9f Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Tue, 2 Nov 2021 18:22:25 +0100 Subject: [PATCH 35/84] :construction: :memo: Work on middleware docs --- http/README.md | 190 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 169 insertions(+), 21 deletions(-) diff --git a/http/README.md b/http/README.md index f88be7997e9a..36b630d2c4d9 100644 --- a/http/README.md +++ b/http/README.md @@ -1,5 +1,3 @@ -- Minimal server -- Handlers - Middleware - Remove Legacy Server @@ -26,40 +24,190 @@ await listenAndServe(":8000", () => new Response("Hello World")); ## Handling Requests -`listenAndServe` expects a `Handler`, which is a function that receives an `HttpRequest` and returns a `Response`: +`listenAndServe` expects a `Handler`, which is a function that receives an [`HttpRequest`](https://doc.deno.land/https/deno.land/std/http/mod.ts#HttpRequest) and returns a [`Response`](https://doc.deno.land/builtin/stable#Response): ```typescript export type Handler = (request: HttpRequest) => Response | Promise; ``` -`std/http` follows web standards, specifically parts of the Fetch API. `HttpRequest` is an extenstion of the -[`Request` web standard](TODO MDN LINK), adding connection information and some helper functions to make it +`std/http` follows web standards, specifically parts of the Fetch API. +[`Request` web standard](https://developer.mozilla.org/en-US/docs/Web/API/Request), adding connection information and some helper functions to make it more convenient to use on servers. The expected return value follows the same standard and is expected to be -a [`Response`](TODO MDN LINK). +a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). -Here is an example handler that echoes the request body +Here is an example handler that echoes the request body back: -## File Server - -A small program for serving local files over HTTP. - -```sh -deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts -> HTTP server listening on http://localhost:4507/ +```typescript +const handle: Handler = (req) => { + return new Response( + req.body, + { status: 200 }, + ) +} ``` ## HTTP Status Code and Status Text -Helper for processing status code and status text. +`Status` offers constants for standard HTTP status codes: ```ts -import { - Status, - STATUS_TEXT, -} from "https://deno.land/std@$STD_VERSION/http/http_status.ts"; +import { listenAndServe, Status } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; + +function handle(req: HttpRequest) { + if (req.method !== "GET") { + // Will respond with an empty 404 + return new Reponse(null, { status: Status.NotFound }); + } + + return new Response("Hello!"); +} + +await listenAndServe(":8000", handle); +``` + +## Middleware + +Middleware is a common pattern to include recurring logic done on requests and responses like deserialization, compression, validation, CORS etc. + +A middleware is a special kind of `Handler` that can pass control on to a next handler during its flow, allowing it to only solve a specific part of handling +the request without needing to know about the rest. As the handler it passes onto can be a middleware itself, this allows to build chains like this: + +``` +Request -----------------------------------------> + +log - authenticate - parseJson - validate - handle + +<-----------------------------------------Response +``` + +Middleware is just code - so it can do anything a normal handler could do, except that it **can** call the next handler. Middleware will sometimes be used +to ensure that some condition is met before passing the request on (e.g. authentication, validation), to pre-process requests in some way to make handling +it simpler and less repetitive (deserialization, database preloading) or to format responses in some way (CORS, compression). + +`std/http` has a simple, yet powerful, strongly typed middleware system: + +### Using Middleware + +To chain middleware, use the `chain()` function: + +```typescript +import { chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +import { auth, cors } from "./my_middleware.ts"; + +function sayHello() { + return new Response("Hello"); +} + +const handler = chain(auth) + .add(cors) + .add(sayHello); + +await listenAndServe(":8000", handler); +``` + +This will pass requests through `auth`, which passes on to `cors`, which passes them on to `sayHello`, with the response from `sayHello` taking the reverse way. + +A chain is itself just a middleware again, so you can pass around and nest chains as much as you like. + +### Request Context + +Request context is a way to pass additional data between middlewares. Each `HttpRequest`s has an attached `context` object. Arbitrary properties with arbitrary +data can be added to the context via the `.addContext()` method. + +Contexts are very strictly typed to prevent runtime errors due to missing context data. + -console.log(Status.NotFound); //=> 404 -console.log(STATUS_TEXT.get(Status.NotFound)); //=> "Not Found" +### Writing Middleware + +Writing middleware is pretty straightforward, there are just two things you need to decide upfront: + +1. Does your middleware depend on any specific context data of previous middleware? +2. Does your middleware add any data to the context for it's following middleware to consume? + +Then you write a function using the `Middleware` type, which takes the two points above as optional type arguments, defaulting to nothing / the `EmptyContext`: + +A simple middleware that logs requests to the stdout could be written like this: + +```typescript +import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; + +export const log: Middleware = async (req, next) => { + const start = performance.now(); + const res = await next(req); + const duration = perormance.now() - start; + + console.log(`${req.method} ${req.url} - ${res.status}, ${duration.toFixed(1)}ms`); + + return res; +}; +``` + +A middleware that ensures the incoming payload is yaml and parses it into the request context as `data` for following middleware to consume could be written like this: + +```typescript +import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +import { parse } from "https://deno.land/std@$STD_VERSION/encoding/yaml.ts"; + +export const yaml: Middleware<{}, { data: unknown }> = async ( + req, + next, +) => { + const rawBody = await req.text() + const data = parse(rawBody) + const newReq = req.addContext({ data }) + + return await next!(newReq); +}; +``` + +The Typescript compiler will make sure that you actually pass the `data` context that you decided onto the `next()` handler. + +Let's write a middleware that will later depend on that `data` property, validating that it is a list of strings: + +```typescript +import { Middleware } from "../../../middleware.ts"; + +export const validate: Middleware<{ data: unknown }> = async ( + req, + next, +) => { + const { data } = req.context + + if (Array.isArray(data) && data.every(it => typeof it === "string")) { + return await next!(req) + } + + return new Response( + "Invalid input, expected an array of string", + { status: 422 }, + ) +}; +``` + +Without explicitly declaring in the `Middleware` type that you depend on a certain piece of context data, Typescript will not let you access it on the actual request context object. + +### Chain Type Safety + +Middleware chains built with the `chain()` function are type safe and order-aware regarding request context, even for arbitrary nesting. + +This means that Typescript will error if you try to use a chain as a handler for e.g. `listenAndServe` if that chain does not satisfy all it's internal context requirements itself in the right order. An example using the two middleares we wrote above: + +```typescript +function handler(req: HttpRequest<{ data: unknown }>): Response { + +} +``` + +```typescript +``` + +## File Server + +A small program for serving local files over HTTP. + +```sh +deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts +> HTTP server listening on http://localhost:4507/ ``` ## Cookie From b16f9e0330ae4a00cb824ccb5dc6660cf4064f96 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Tue, 2 Nov 2021 18:28:03 +0100 Subject: [PATCH 36/84] :recycle: Add EmptyContext for middlewares --- http/middleware.ts | 25 +++++++++++++------------ http/middleware/json.ts | 6 ++++-- http/middleware/log.ts | 6 ++++-- http/mod.ts | 3 ++- http/request.ts | 9 +++++++-- 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index 4195a7ebe583..1b5917b3ae75 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,6 +1,7 @@ // deno-lint-ignore-file ban-types import { HttpRequest } from "./request.ts"; +import type { EmptyContext } from "./request.ts"; import type { Expand, SafeOmit } from "../_util/types.ts"; /** @@ -11,10 +12,10 @@ import type { Expand, SafeOmit } from "../_util/types.ts"; * available to the `next` middleware. * * @typeParam Needs - Request context required by this Middleware, defaults to -* empty context `{}` +* `EmptyContext` which is the empty object * * @typeParam Adds - Request context this Middleware will add and thus make -* available to the `next` Middleware, defaults to empty context `{}` +* available to the `next` Middleware, defaults to `EmptyContext` which is the empty object * * Example: * @@ -35,8 +36,8 @@ import type { Expand, SafeOmit } from "../_util/types.ts"; * ``` */ export type Middleware< - Needs extends {} = {}, - Adds = {}, + Needs extends EmptyContext = EmptyContext, + Adds = EmptyContext, > = ( req: HttpRequest, next?: Middleware>, @@ -46,8 +47,8 @@ export type Middleware< * A `Middleware` that can be chained onto with `.add()`. Use `chain()` to wrap * a `Middleware` as a `MiddlewareChain`. */ export type MiddlewareChain< - Needs extends {}, - Adds = {}, + Needs extends EmptyContext, + Adds = EmptyContext, > = { ( ...args: Parameters> @@ -92,7 +93,7 @@ export type MiddlewareChain< * Example: * * ```ts - * const findUser: Middleware<{}, { user: string }> = (req, next) => { + * const findUser: Middleware = (req, next) => { * return await next!( * req.addContext({ user: "Kim" }); * ); @@ -104,10 +105,10 @@ export type MiddlewareChain< * const handler = composeMiddleware(findUser, hello) * ``` */ export function composeMiddleware< - FirstNeeds extends {}, - FirstAdd extends {}, - SecondNeeds extends {}, - SecondAdd extends {}, + FirstNeeds extends EmptyContext, + FirstAdd extends EmptyContext, + SecondNeeds extends EmptyContext, + SecondAdd extends EmptyContext, >( first: Middleware, second: Middleware, @@ -128,7 +129,7 @@ export function composeMiddleware< } /** Wraps the given middleware in a `MiddlewareChain` so it can be `.add()`ed onto */ -export function chain( +export function chain( middleware: Middleware, ): MiddlewareChain { const copy = middleware.bind({}) as MiddlewareChain; diff --git a/http/middleware/json.ts b/http/middleware/json.ts index 78df45b919fa..ec80ec760ecc 100644 --- a/http/middleware/json.ts +++ b/http/middleware/json.ts @@ -1,7 +1,9 @@ import { Middleware } from "../middleware.ts"; -// deno-lint-ignore ban-types -export const acceptJson: Middleware<{}, { parsedBody: unknown }> = async ( +export const acceptJson: Middleware< + Record, + { parsedBody: unknown } +> = async ( req, next, ) => { diff --git a/http/middleware/log.ts b/http/middleware/log.ts index 7577f46fecb3..906d6d27f583 100644 --- a/http/middleware/log.ts +++ b/http/middleware/log.ts @@ -6,8 +6,10 @@ export const log: Middleware = async (req, next) => { const end = performance.now(); console.log( - `${req.method} ${new URL(req.url).pathname}\t${response.status}\t${end - - start}ms`, + `${req.method} ${new URL(req.url).pathname}\t${response.status}\t${ + end - + start + }ms`, ); return response; diff --git a/http/mod.ts b/http/mod.ts index 1eff0e5be978..dfae41d227df 100644 --- a/http/mod.ts +++ b/http/mod.ts @@ -1,5 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. export * from "./cookie.ts"; export * from "./http_status.ts"; -export * from "./server.ts"; +export * from "./middleware.ts"; export * from "./request.ts"; +export * from "./server.ts"; diff --git a/http/request.ts b/http/request.ts index e1c298902fad..0f97cea16065 100644 --- a/http/request.ts +++ b/http/request.ts @@ -1,12 +1,15 @@ import { ConnInfo } from "./server.ts"; import { Expand } from "../_util/types.ts"; +export type EmptyContext = Record; + /** * An incoming request. Follows the Request web standard, adding connection * information, a mutable request context and some helpers for common use cases. * * @typeParam C - Type of the current request `context` */ -export class HttpRequest implements Request { +export class HttpRequest + implements Request { #context: C; #parsedUrl?: URL = undefined; @@ -46,7 +49,9 @@ export class HttpRequest implements Request { * assertEquals(reqWithUser.context.user, "Example") * ``` */ - addContext(contextToAdd: N): HttpRequest> { + addContext( + contextToAdd: N, + ): HttpRequest> { this.#context = { ...this.#context, ...contextToAdd }; //@ts-ignore Limitations of mutation and types, but we should mutate for performance From a162adcd48f9f8da1b4b18d081702139331c51d4 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Tue, 2 Nov 2021 18:29:34 +0100 Subject: [PATCH 37/84] :memo: Add EmptyContext to middleware docs --- http/README.md | 157 ++++++++++++++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 60 deletions(-) diff --git a/http/README.md b/http/README.md index 36b630d2c4d9..20273b70ee06 100644 --- a/http/README.md +++ b/http/README.md @@ -10,7 +10,9 @@ # http -Deno's standard HTTP server based on the [blazing fast native http API](https://deno.land/manual/runtime/http_server_apis#http-server-apis) under the hood. +Deno's standard HTTP server based on the +[blazing fast native http API](https://deno.land/manual/runtime/http_server_apis#http-server-apis) +under the hood. ## Minimal Server @@ -24,42 +26,49 @@ await listenAndServe(":8000", () => new Response("Hello World")); ## Handling Requests -`listenAndServe` expects a `Handler`, which is a function that receives an [`HttpRequest`](https://doc.deno.land/https/deno.land/std/http/mod.ts#HttpRequest) and returns a [`Response`](https://doc.deno.land/builtin/stable#Response): +`listenAndServe` expects a `Handler`, which is a function that receives an +[`HttpRequest`](https://doc.deno.land/https/deno.land/std/http/mod.ts#HttpRequest) +and returns a [`Response`](https://doc.deno.land/builtin/stable#Response): ```typescript export type Handler = (request: HttpRequest) => Response | Promise; ``` -`std/http` follows web standards, specifically parts of the Fetch API. -[`Request` web standard](https://developer.mozilla.org/en-US/docs/Web/API/Request), adding connection information and some helper functions to make it -more convenient to use on servers. The expected return value follows the same standard and is expected to be -a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). +`std/http` follows web standards, specifically parts of the Fetch API. +[`Request` web standard](https://developer.mozilla.org/en-US/docs/Web/API/Request), +adding connection information and some helper functions to make it more +convenient to use on servers. The expected return value follows the same +standard and is expected to be a +[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). Here is an example handler that echoes the request body back: ```typescript const handle: Handler = (req) => { - return new Response( - req.body, - { status: 200 }, - ) -} + return new Response( + req.body, + { status: 200 }, + ); +}; ``` ## HTTP Status Code and Status Text -`Status` offers constants for standard HTTP status codes: +`Status` offers constants for standard HTTP status codes: ```ts -import { listenAndServe, Status } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +import { + listenAndServe, + Status, +} from "https://deno.land/std@$STD_VERSION/http/mod.ts"; function handle(req: HttpRequest) { - if (req.method !== "GET") { - // Will respond with an empty 404 - return new Reponse(null, { status: Status.NotFound }); - } + if (req.method !== "GET") { + // Will respond with an empty 404 + return new Reponse(null, { status: Status.NotFound }); + } - return new Response("Hello!"); + return new Response("Hello!"); } await listenAndServe(":8000", handle); @@ -67,10 +76,13 @@ await listenAndServe(":8000", handle); ## Middleware -Middleware is a common pattern to include recurring logic done on requests and responses like deserialization, compression, validation, CORS etc. +Middleware is a common pattern to include recurring logic done on requests and +responses like deserialization, compression, validation, CORS etc. -A middleware is a special kind of `Handler` that can pass control on to a next handler during its flow, allowing it to only solve a specific part of handling -the request without needing to know about the rest. As the handler it passes onto can be a middleware itself, this allows to build chains like this: +A middleware is a special kind of `Handler` that can pass control on to a next +handler during its flow, allowing it to only solve a specific part of handling +the request without needing to know about the rest. As the handler it passes +onto can be a middleware itself, this allows to build chains like this: ``` Request -----------------------------------------> @@ -80,9 +92,12 @@ log - authenticate - parseJson - validate - handle <-----------------------------------------Response ``` -Middleware is just code - so it can do anything a normal handler could do, except that it **can** call the next handler. Middleware will sometimes be used -to ensure that some condition is met before passing the request on (e.g. authentication, validation), to pre-process requests in some way to make handling -it simpler and less repetitive (deserialization, database preloading) or to format responses in some way (CORS, compression). +Middleware is just code - so it can do anything a normal handler could do, +except that it **can** call the next handler. Middleware will sometimes be used +to ensure that some condition is met before passing the request on (e.g. +authentication, validation), to pre-process requests in some way to make +handling it simpler and less repetitive (deserialization, database preloading) +or to format responses in some way (CORS, compression). `std/http` has a simple, yet powerful, strongly typed middleware system: @@ -95,36 +110,44 @@ import { chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; import { auth, cors } from "./my_middleware.ts"; function sayHello() { - return new Response("Hello"); + return new Response("Hello"); } const handler = chain(auth) - .add(cors) - .add(sayHello); + .add(cors) + .add(sayHello); await listenAndServe(":8000", handler); ``` -This will pass requests through `auth`, which passes on to `cors`, which passes them on to `sayHello`, with the response from `sayHello` taking the reverse way. +This will pass requests through `auth`, which passes on to `cors`, which passes +them on to `sayHello`, with the response from `sayHello` taking the reverse way. -A chain is itself just a middleware again, so you can pass around and nest chains as much as you like. +A chain is itself just a middleware again, so you can pass around and nest +chains as much as you like. ### Request Context -Request context is a way to pass additional data between middlewares. Each `HttpRequest`s has an attached `context` object. Arbitrary properties with arbitrary -data can be added to the context via the `.addContext()` method. - -Contexts are very strictly typed to prevent runtime errors due to missing context data. +Request context is a way to pass additional data between middlewares. Each +`HttpRequest`s has an attached `context` object. Arbitrary properties with +arbitrary data can be added to the context via the `.addContext()` method. +Contexts are very strictly typed to prevent runtime errors due to missing +context data. ### Writing Middleware -Writing middleware is pretty straightforward, there are just two things you need to decide upfront: +Writing middleware is pretty straightforward, there are just two things you need +to decide upfront: -1. Does your middleware depend on any specific context data of previous middleware? -2. Does your middleware add any data to the context for it's following middleware to consume? +1. Does your middleware depend on any specific context data of previous + middleware? +2. Does your middleware add any data to the context for it's following + middleware to consume? -Then you write a function using the `Middleware` type, which takes the two points above as optional type arguments, defaulting to nothing / the `EmptyContext`: +Then you write a function using the `Middleware` type, which takes the two +points above as optional type arguments, defaulting to nothing / the +`EmptyContext`: A simple middleware that logs requests to the stdout could be written like this: @@ -132,37 +155,46 @@ A simple middleware that logs requests to the stdout could be written like this: import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; export const log: Middleware = async (req, next) => { - const start = performance.now(); - const res = await next(req); - const duration = perormance.now() - start; + const start = performance.now(); + const res = await next(req); + const duration = perormance.now() - start; - console.log(`${req.method} ${req.url} - ${res.status}, ${duration.toFixed(1)}ms`); + console.log( + `${req.method} ${req.url} - ${res.status}, ${duration.toFixed(1)}ms`, + ); - return res; + return res; }; ``` -A middleware that ensures the incoming payload is yaml and parses it into the request context as `data` for following middleware to consume could be written like this: +A middleware that ensures the incoming payload is yaml and parses it into the +request context as `data` for following middleware to consume could be written +like this: ```typescript -import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +import { + EmptyContext, + Middleware, +} from "https://deno.land/std@$STD_VERSION/http/mod.ts"; import { parse } from "https://deno.land/std@$STD_VERSION/encoding/yaml.ts"; -export const yaml: Middleware<{}, { data: unknown }> = async ( +export const yaml: Middleware = async ( req, next, ) => { - const rawBody = await req.text() - const data = parse(rawBody) - const newReq = req.addContext({ data }) + const rawBody = await req.text(); + const data = parse(rawBody); + const newReq = req.addContext({ data }); return await next!(newReq); }; ``` -The Typescript compiler will make sure that you actually pass the `data` context that you decided onto the `next()` handler. +The Typescript compiler will make sure that you actually pass the `data` context +that you decided onto the `next()` handler. -Let's write a middleware that will later depend on that `data` property, validating that it is a list of strings: +Let's write a middleware that will later depend on that `data` property, +validating that it is a list of strings: ```typescript import { Middleware } from "../../../middleware.ts"; @@ -171,30 +203,35 @@ export const validate: Middleware<{ data: unknown }> = async ( req, next, ) => { - const { data } = req.context + const { data } = req.context; - if (Array.isArray(data) && data.every(it => typeof it === "string")) { - return await next!(req) + if (Array.isArray(data) && data.every((it) => typeof it === "string")) { + return await next!(req); } return new Response( - "Invalid input, expected an array of string", - { status: 422 }, - ) + "Invalid input, expected an array of string", + { status: 422 }, + ); }; ``` -Without explicitly declaring in the `Middleware` type that you depend on a certain piece of context data, Typescript will not let you access it on the actual request context object. +Without explicitly declaring in the `Middleware` type that you depend on a +certain piece of context data, Typescript will not let you access it on the +actual request context object. ### Chain Type Safety -Middleware chains built with the `chain()` function are type safe and order-aware regarding request context, even for arbitrary nesting. +Middleware chains built with the `chain()` function are type safe and +order-aware regarding request context, even for arbitrary nesting. -This means that Typescript will error if you try to use a chain as a handler for e.g. `listenAndServe` if that chain does not satisfy all it's internal context requirements itself in the right order. An example using the two middleares we wrote above: +This means that Typescript will error if you try to use a chain as a handler for +e.g. `listenAndServe` if that chain does not satisfy all it's internal context +requirements itself in the right order. An example using the two middleares we +wrote above: ```typescript function handler(req: HttpRequest<{ data: unknown }>): Response { - } ``` From f099b5f250ee6ecffbf34e7d38e4ae25e3652884 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 3 Nov 2021 09:21:10 +0100 Subject: [PATCH 38/84] :sparkles: Add Method enum --- http/README.md | 8 +++++--- http/_io.ts | 2 +- http/file_server.ts | 2 +- http/{http_status.ts => helper_constants.ts} | 12 ++++++++++-- http/mod.ts | 2 +- node/http.ts | 2 +- 6 files changed, 19 insertions(+), 9 deletions(-) rename http/{http_status.ts => helper_constants.ts} (96%) diff --git a/http/README.md b/http/README.md index 20273b70ee06..87eabf3c48cc 100644 --- a/http/README.md +++ b/http/README.md @@ -52,18 +52,20 @@ const handle: Handler = (req) => { }; ``` -## HTTP Status Code and Status Text +## HTTP Status Codes and Methods -`Status` offers constants for standard HTTP status codes: +The `Status` and `Method` enums offers helper constants for standard HTTP status +codes and methods: ```ts import { listenAndServe, Status, + Method, } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; function handle(req: HttpRequest) { - if (req.method !== "GET") { + if (req.method !== Method.Get) { // Will respond with an empty 404 return new Reponse(null, { status: Status.NotFound }); } diff --git a/http/_io.ts b/http/_io.ts index 4e99e574bf58..69ac8d4a2cb8 100644 --- a/http/_io.ts +++ b/http/_io.ts @@ -4,7 +4,7 @@ import { copy, iterateReader } from "../streams/conversion.ts"; import { TextProtoReader } from "../textproto/mod.ts"; import { assert } from "../_util/assert.ts"; import { Response, ServerRequest } from "./server_legacy.ts"; -import { STATUS_TEXT } from "./http_status.ts"; +import { STATUS_TEXT } from "./helper_constants.ts"; const encoder = new TextEncoder(); diff --git a/http/file_server.ts b/http/file_server.ts index ff6aa2b421a9..cf8f87481a60 100644 --- a/http/file_server.ts +++ b/http/file_server.ts @@ -7,7 +7,7 @@ import { extname, posix } from "../path/mod.ts"; import { listenAndServe, listenAndServeTls } from "./server.ts"; -import { Status, STATUS_TEXT } from "./http_status.ts"; +import { Status, STATUS_TEXT } from "./helper_constants.ts"; import { parse } from "../flags/mod.ts"; import { assert } from "../_util/assert.ts"; import { red } from "../fmt/colors.ts"; diff --git a/http/http_status.ts b/http/helper_constants.ts similarity index 96% rename from http/http_status.ts rename to http/helper_constants.ts index 734e54a1a624..e6f9030263df 100644 --- a/http/http_status.ts +++ b/http/helper_constants.ts @@ -1,5 +1,13 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +export enum Method { + Get = "GET", + Post = "POST", + Put = "PUT", + Delete = "DELETE", + Head = "HEAD", + Options = "OPTIONS", +} /** * Enum of HTTP status codes. * @@ -7,7 +15,7 @@ * import { * Status, * STATUS_TEXT, - * } from "https://deno.land/std@$STD_VERSION/http/http_status.ts"; + * } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * console.log(Status.NotFound); //=> 404 * console.log(STATUS_TEXT.get(Status.NotFound)); //=> "Not Found" @@ -150,7 +158,7 @@ export enum Status { * import { * Status, * STATUS_TEXT, - * } from "https://deno.land/std@$STD_VERSION/http/http_status.ts"; + * } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * console.log(Status.NotFound); //=> 404 * console.log(STATUS_TEXT.get(Status.NotFound)); //=> "Not Found" diff --git a/http/mod.ts b/http/mod.ts index dfae41d227df..825c544a99ae 100644 --- a/http/mod.ts +++ b/http/mod.ts @@ -1,6 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. export * from "./cookie.ts"; -export * from "./http_status.ts"; +export * from "./helper_constants.ts"; export * from "./middleware.ts"; export * from "./request.ts"; export * from "./server.ts"; diff --git a/node/http.ts b/node/http.ts index cf69922da507..b9bb357ce71f 100644 --- a/node/http.ts +++ b/node/http.ts @@ -1,4 +1,4 @@ -import { Status as STATUS_CODES } from "../http/http_status.ts"; +import { Status as STATUS_CODES } from "../http/helper_constants.ts"; import { Buffer } from "./buffer.ts"; import { EventEmitter } from "./events.ts"; import NodeReadable from "./_stream/readable.ts"; From 65c4cf6c02b9f0ca38cfd638de2050c5db8287d4 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 3 Nov 2021 09:38:46 +0100 Subject: [PATCH 39/84] :memo: First draft of middleware docs --- http/README.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/http/README.md b/http/README.md index 87eabf3c48cc..69df316a5711 100644 --- a/http/README.md +++ b/http/README.md @@ -35,9 +35,10 @@ export type Handler = (request: HttpRequest) => Response | Promise; ``` `std/http` follows web standards, specifically parts of the Fetch API. +`HttpRequest` is an extension of the [`Request` web standard](https://developer.mozilla.org/en-US/docs/Web/API/Request), adding connection information and some helper functions to make it more -convenient to use on servers. The expected return value follows the same +convenient to use on servers. The expected return value follows the same Fetch standard and is expected to be a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). @@ -59,19 +60,20 @@ codes and methods: ```ts import { + Handler, listenAndServe, - Status, Method, + Status, } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; -function handle(req: HttpRequest) { +const handle: Handler = (req) => { if (req.method !== Method.Get) { // Will respond with an empty 404 return new Reponse(null, { status: Status.NotFound }); } return new Response("Hello!"); -} +}; await listenAndServe(":8000", handle); ``` @@ -233,11 +235,31 @@ requirements itself in the right order. An example using the two middleares we wrote above: ```typescript -function handler(req: HttpRequest<{ data: unknown }>): Response { -} +const handle: Handler<{ data: unknown {> = (req) => { + console.dir(req.context.data); + + return new Response("Thank you for your strings"); +}; ``` +This will not pass the type checker: + ```typescript +const handleStringArray = chain(validate) + .add(yaml) + .add(handle); + +await listenAndServe(":8000", handleStringArray); +``` + +But this will: + +```typescript +const handleStringArray = chain(yaml) + .add(validate) + .add(handle); + +await listenAndServe(":8000", handleStringArray); ``` ## File Server From abf63f2082eca8cad9374a852bb2a0bb61839997 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 3 Nov 2021 09:45:15 +0100 Subject: [PATCH 40/84] :memo: Add chain nesting example --- http/README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/http/README.md b/http/README.md index 69df316a5711..44a7049c192b 100644 --- a/http/README.md +++ b/http/README.md @@ -128,7 +128,25 @@ This will pass requests through `auth`, which passes on to `cors`, which passes them on to `sayHello`, with the response from `sayHello` taking the reverse way. A chain is itself just a middleware again, so you can pass around and nest -chains as much as you like. +chains as much as you like. This (nonsensical) example does the exact same as +the one above: + +```typescript +import { chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +import { auth, cors } from "./my_middleware.ts"; + +function sayHello() { + return new Response("Hello"); +} + +const core = chain(cors) + .add(sayHello); + +const handler = chain(auth) + .add(core); + +await listenAndServe(":8000", handler); +``` ### Request Context From ce70eeb98c58fae46566384411cf1c29c6f55f34 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 3 Nov 2021 11:22:36 +0100 Subject: [PATCH 41/84] :sparkles: Add correct context merging to middleware types --- http/middleware.ts | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index 1b5917b3ae75..2ba232c7f701 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,8 +1,6 @@ -// deno-lint-ignore-file ban-types - import { HttpRequest } from "./request.ts"; import type { EmptyContext } from "./request.ts"; -import type { Expand, SafeOmit } from "../_util/types.ts"; +import type { CommonKeys, Expand, SafeOmit } from "../_util/types.ts"; /** * Middleware Handler that is expected to call the `next` Middleware at some @@ -37,10 +35,10 @@ import type { Expand, SafeOmit } from "../_util/types.ts"; */ export type Middleware< Needs extends EmptyContext = EmptyContext, - Adds = EmptyContext, + Adds extends EmptyContext = EmptyContext, > = ( req: HttpRequest, - next?: Middleware>, + next?: Middleware>>, ) => Promise; /** @@ -48,7 +46,7 @@ export type Middleware< * a `Middleware` as a `MiddlewareChain`. */ export type MiddlewareChain< Needs extends EmptyContext, - Adds = EmptyContext, + Adds extends EmptyContext = EmptyContext, > = { ( ...args: Parameters> @@ -82,8 +80,8 @@ export type MiddlewareChain< add( middleware: Middleware, ): MiddlewareChain< - Expand>, - Expand + Expand>, // todo does safeomit handle assignability? e.g. require string, provide 'a' + Expand> >; }; @@ -114,7 +112,7 @@ export function composeMiddleware< second: Middleware, ): Middleware< Expand>, - Expand + Expand> > { return (req, next) => first( @@ -129,7 +127,10 @@ export function composeMiddleware< } /** Wraps the given middleware in a `MiddlewareChain` so it can be `.add()`ed onto */ -export function chain( +export function chain< + Needs extends EmptyContext, + Adds extends EmptyContext = EmptyContext, +>( middleware: Middleware, ): MiddlewareChain { const copy = middleware.bind({}) as MiddlewareChain; @@ -144,3 +145,26 @@ export function chain( return copy; } + +type LeftDistinct< + Left extends EmptyContext, + Right extends EmptyContext, +> = { + [Prop in keyof Omit]: Left[Prop]; +}; + +type CommonMerge< + Left extends EmptyContext, + Right extends EmptyContext, +> = { + [Prop in CommonKeys]: Right[Prop] extends never ? Left[Prop] + : Right[Prop]; +}; + +type MergeContext< + Left extends EmptyContext, + Right extends EmptyContext, +> = + & CommonMerge + & LeftDistinct + & LeftDistinct; From 71de170bc5677e629f960764716ffe74b8a482e8 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Fri, 5 Nov 2021 17:55:19 +0100 Subject: [PATCH 42/84] :memo: Add type enhancement example to docs --- http/README.md | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/http/README.md b/http/README.md index 44a7049c192b..554aa84c3fe5 100644 --- a/http/README.md +++ b/http/README.md @@ -1,4 +1,4 @@ -- Middleware +- Type enhancements in middleware - Remove Legacy Server # PR @@ -216,26 +216,33 @@ The Typescript compiler will make sure that you actually pass the `data` context that you decided onto the `next()` handler. Let's write a middleware that will later depend on that `data` property, -validating that it is a list of strings: +validating that it is a list of strings. Note that we also change the context +here, overriding the `data` property to be `string[]` after this middleware +instead of `unknown`, which will allow following code to work with it safely: ```typescript import { Middleware } from "../../../middleware.ts"; -export const validate: Middleware<{ data: unknown }> = async ( - req, - next, -) => { - const { data } = req.context; - - if (Array.isArray(data) && data.every((it) => typeof it === "string")) { - return await next!(req); - } - - return new Response( - "Invalid input, expected an array of string", - { status: 422 }, - ); -}; +export const validate: Middleware<{ data: unknown }, { data: string[] }> = + async ( + req, + next, + ) => { + const { data } = req.context; + + if (Array.isArray(data) && data.every((it) => typeof it === "string")) { + const newReq = req.addContext({ + data: data as string[], + }); + + return await next!(newReq); + } + + return new Response( + "Invalid input, expected an array of string", + { status: 422 }, + ); + }; ``` Without explicitly declaring in the `Middleware` type that you depend on a From 5fa983fe8f63449694da2d7420088710edb41116 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sat, 6 Nov 2021 21:03:06 +0100 Subject: [PATCH 43/84] :memo: Format middleware examples --- http/README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/http/README.md b/http/README.md index 554aa84c3fe5..7c1f1ab2c772 100644 --- a/http/README.md +++ b/http/README.md @@ -224,10 +224,7 @@ instead of `unknown`, which will allow following code to work with it safely: import { Middleware } from "../../../middleware.ts"; export const validate: Middleware<{ data: unknown }, { data: string[] }> = - async ( - req, - next, - ) => { + async (req, next) => { const { data } = req.context; if (Array.isArray(data) && data.every((it) => typeof it === "string")) { @@ -260,10 +257,10 @@ requirements itself in the right order. An example using the two middleares we wrote above: ```typescript -const handle: Handler<{ data: unknown {> = (req) => { - console.dir(req.context.data); +const handle: Handler<{ data: string }> = (req) => { + console.dir(req.context.data); - return new Response("Thank you for your strings"); + return new Response("Thank you for your strings"); }; ``` From ff22ab9138d6fd971c589246a957eaeee1bda963 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sat, 6 Nov 2021 23:20:10 +0100 Subject: [PATCH 44/84] :memo: Improve middleware doc formatting --- http/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/http/README.md b/http/README.md index 7c1f1ab2c772..8afd6282ccd1 100644 --- a/http/README.md +++ b/http/README.md @@ -168,10 +168,8 @@ to decide upfront: middleware to consume? Then you write a function using the `Middleware` type, which takes the two -points above as optional type arguments, defaulting to nothing / the -`EmptyContext`: - -A simple middleware that logs requests to the stdout could be written like this: +points above as optional type arguments, defaulting to the `EmptyContext` (which +is an empty object). A simple middleware that logs requests could be written like this: ```typescript import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; From e328ded43d3ea46f2aefb9d0d02048273d4b9bd5 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 00:25:16 +0100 Subject: [PATCH 45/84] :label: Make next not optional for DX middleware --- http/middleware.ts | 11 ++++++----- http/server.ts | 6 ++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index 2ba232c7f701..0dcd746c36ee 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,5 +1,5 @@ -import { HttpRequest } from "./request.ts"; -import type { EmptyContext } from "./request.ts"; +import type { Handler } from "./server.ts"; +import type { HttpRequest, EmptyContext } from "./request.ts"; import type { CommonKeys, Expand, SafeOmit } from "../_util/types.ts"; /** @@ -38,8 +38,8 @@ export type Middleware< Adds extends EmptyContext = EmptyContext, > = ( req: HttpRequest, - next?: Middleware>>, -) => Promise; + next: Handler>>, +) => Response | Promise; /** * A `Middleware` that can be chained onto with `.add()`. Use `chain()` to wrap @@ -49,7 +49,8 @@ export type MiddlewareChain< Adds extends EmptyContext = EmptyContext, > = { ( - ...args: Parameters> + req: Parameters>[0], + next?: Parameters>[1], ): ReturnType>; /** diff --git a/http/server.ts b/http/server.ts index b4e0694930e7..e4503d81dc2d 100644 --- a/http/server.ts +++ b/http/server.ts @@ -1,6 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. import { delay } from "../async/mod.ts"; -import { HttpRequest } from "./request.ts"; +import { EmptyContext, HttpRequest } from "./request.ts"; /** Thrown by Server after it has been closed. */ const ERROR_SERVER_CLOSED = "Server closed"; @@ -36,7 +36,9 @@ export interface ConnInfo { * of the error is isolated to the individual request. It will catch the error * and close the underlying connection. */ -export type Handler = (request: HttpRequest) => Response | Promise; +export type Handler = ( + request: HttpRequest, +) => Response | Promise; /** * Parse an address from a string. From 10e08fd7fe4095275a406a8610e1ae090ed1e58f Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 00:25:46 +0100 Subject: [PATCH 46/84] :memo: More work on middleware docs --- http/README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/http/README.md b/http/README.md index 8afd6282ccd1..3fa1d9544061 100644 --- a/http/README.md +++ b/http/README.md @@ -1,4 +1,5 @@ -- Type enhancements in middleware +# Todos + - Remove Legacy Server # PR @@ -159,8 +160,11 @@ context data. ### Writing Middleware -Writing middleware is pretty straightforward, there are just two things you need -to decide upfront: +Writing a middleware is the same as writing a `Handler`, except that it gets +passed an additional argument, which is the rest of the chain and should be called +to pass control on. + +To write middleware in typescript, there are two core things to decide upfront: 1. Does your middleware depend on any specific context data of previous middleware? @@ -169,7 +173,8 @@ to decide upfront: Then you write a function using the `Middleware` type, which takes the two points above as optional type arguments, defaulting to the `EmptyContext` (which -is an empty object). A simple middleware that logs requests could be written like this: +is an empty object). A simple middleware that logs requests could be written +like this: ```typescript import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; @@ -177,7 +182,7 @@ import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; export const log: Middleware = async (req, next) => { const start = performance.now(); const res = await next(req); - const duration = perormance.now() - start; + const duration = performance.now() - start; console.log( `${req.method} ${req.url} - ${res.status}, ${duration.toFixed(1)}ms`, @@ -187,6 +192,11 @@ export const log: Middleware = async (req, next) => { }; ``` +Note that because we neither depend on any context data nor add any ourselves, +we can use the bare `Middleware` type here, which is short for +`Middleware`, meaning we depend on the empty context +and add nothing. + A middleware that ensures the incoming payload is yaml and parses it into the request context as `data` for following middleware to consume could be written like this: From 48d8aaad98a7285d18b4c92e04b89a9f2d299612 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 00:33:34 +0100 Subject: [PATCH 47/84] :memo: Adapt docs to recent DX changes --- http/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/http/README.md b/http/README.md index 3fa1d9544061..f4f86aa1b039 100644 --- a/http/README.md +++ b/http/README.md @@ -162,9 +162,9 @@ context data. Writing a middleware is the same as writing a `Handler`, except that it gets passed an additional argument, which is the rest of the chain and should be called -to pass control on. +to pass control on. Canonically, that parameter is called `next`. -To write middleware in typescript, there are two core things to decide upfront: +To write middleware in typescript, there are two things to decide upfront: 1. Does your middleware depend on any specific context data of previous middleware? @@ -216,7 +216,7 @@ export const yaml: Middleware = async ( const data = parse(rawBody); const newReq = req.addContext({ data }); - return await next!(newReq); + return await next(newReq); }; ``` @@ -232,7 +232,7 @@ instead of `unknown`, which will allow following code to work with it safely: import { Middleware } from "../../../middleware.ts"; export const validate: Middleware<{ data: unknown }, { data: string[] }> = - async (req, next) => { + (req, next) => { const { data } = req.context; if (Array.isArray(data) && data.every((it) => typeof it === "string")) { @@ -240,7 +240,7 @@ export const validate: Middleware<{ data: unknown }, { data: string[] }> = data: data as string[], }); - return await next!(newReq); + return next(newReq); } return new Response( From 497013bc569a70223392bf1f2c788900069430f2 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 00:56:45 +0100 Subject: [PATCH 48/84] :fire: Remove WIP files --- http/middleware/example.ts | 60 +++++++++++++++++++++++++++++ http/middleware/json.ts | 37 ------------------ http/middleware/log.ts | 16 -------- http/middleware/poc/server.ts | 42 -------------------- http/middleware/poc/validate_zoo.ts | 38 ------------------ http/middleware/poc/zoo.ts | 18 --------- 6 files changed, 60 insertions(+), 151 deletions(-) create mode 100644 http/middleware/example.ts delete mode 100644 http/middleware/json.ts delete mode 100644 http/middleware/log.ts delete mode 100644 http/middleware/poc/server.ts delete mode 100644 http/middleware/poc/validate_zoo.ts delete mode 100644 http/middleware/poc/zoo.ts diff --git a/http/middleware/example.ts b/http/middleware/example.ts new file mode 100644 index 000000000000..ff8adbc9186c --- /dev/null +++ b/http/middleware/example.ts @@ -0,0 +1,60 @@ +import { + chain, + EmptyContext, + Handler, + listenAndServe, + Middleware, +} from "../mod.ts"; +import { parse } from "../../encoding/yaml.ts"; + +const log: Middleware = async (req, next) => { + const start = performance.now(); + const res = await next(req); + const duration = performance.now() - start; + + console.log( + `${req.method} ${req.url} - ${res.status}, ${duration.toFixed(1)}ms`, + ); + + return res; +}; + +const yaml: Middleware = async ( + req, + next, +) => { + const rawBody = await req.text(); + const data = parse(rawBody); + const newReq = req.addContext({ data }); + + return await next(newReq); +}; + +const validate: Middleware<{ data: unknown }, { data: string[] }> = ( + req, + next, +) => { + const { data } = req.context; + + if (Array.isArray(data) && data.every((it) => typeof it === "string")) { + return next(req.addContext({ data })); + } + + return new Response( + "Invalid input, expected an array of string", + { status: 422 }, + ); +}; + +const handle: Handler<{ data: string[] }> = (req) => { + const { data } = req.context; + + return new Response(data.map((it) => `Hello ${it}`).join("\n")); +}; + +const stack = chain(log) + .add(yaml) + .add(validate) + .add(handle); + +await listenAndServe(":8000", stack); diff --git a/http/middleware/json.ts b/http/middleware/json.ts deleted file mode 100644 index ec80ec760ecc..000000000000 --- a/http/middleware/json.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Middleware } from "../middleware.ts"; - -export const acceptJson: Middleware< - Record, - { parsedBody: unknown } -> = async ( - req, - next, -) => { - if (!req.headers.get("content-type")?.includes("application/json")) { - return new Response( - "Content Type not supported, expected application/json", - { status: 415 }, - ); - } - - const body = await req.text(); - - let parsedBody: unknown; - - try { - parsedBody = JSON.parse(body); - } catch (e) { - if (e instanceof SyntaxError) { - return new Response( - `Request could not be parsed as JSON: ${e.message}`, - { status: 422 }, - ); - } - - throw e; - } - - const nextReq = req.addContext({ parsedBody }); - - return next!(nextReq); -}; diff --git a/http/middleware/log.ts b/http/middleware/log.ts deleted file mode 100644 index 906d6d27f583..000000000000 --- a/http/middleware/log.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Middleware } from "../middleware.ts"; - -export const log: Middleware = async (req, next) => { - const start = performance.now(); - const response = await next!(req); - const end = performance.now(); - - console.log( - `${req.method} ${new URL(req.url).pathname}\t${response.status}\t${ - end - - start - }ms`, - ); - - return response; -}; diff --git a/http/middleware/poc/server.ts b/http/middleware/poc/server.ts deleted file mode 100644 index 0c038efa9489..000000000000 --- a/http/middleware/poc/server.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { distinctBy } from "../../../collections/mod.ts"; -import { listenAndServe } from "../../server.ts"; -import { acceptJson } from "../json.ts"; -import { log } from "../log.ts"; -import { chain } from "../../middleware.ts"; -import { HttpRequest } from "../../request.ts"; -import { validateZoo } from "./validate_zoo.ts"; -import type { Zoo } from "./zoo.ts"; - -function createZoo(req: HttpRequest<{ zoo: Zoo }>) { - const { zoo } = req.context; - const responseMessage = ` - Your nice ${zoo.name} Zoo was created. - - Take good care of your ${zoo.animals.length} animals! - All those ${ - distinctBy(zoo.animals, (it) => it.kind).map((it) => `${it.kind}s`).join( - " and ", - ) - } will surely amaze your visitors. - `; - - return Promise.resolve(new Response(responseMessage, { status: 201 })); -} - -function passThrough( - req: HttpRequest, - next?: (r: HttpRequest) => Promise, -) { - return next!(req); -} - -const handleCreateZoo = chain(log) - .add(passThrough) - .add(acceptJson) - .add(validateZoo) - .add(createZoo); - -await listenAndServe( - "0.0.0.0:5000", - handleCreateZoo, -); diff --git a/http/middleware/poc/validate_zoo.ts b/http/middleware/poc/validate_zoo.ts deleted file mode 100644 index d08e9a1f0c2e..000000000000 --- a/http/middleware/poc/validate_zoo.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Middleware } from "../../middleware.ts"; -import { includesValue } from "../../../collections/includes_value.ts"; -import { Animal, AnimalKind, Zoo } from "./zoo.ts"; - -function isZoo(subject: unknown): subject is Zoo { - const cast = subject as Zoo; - - return typeof cast === "object" && - typeof cast.name === "string" && - typeof cast.entryFee === "number" && - Array.isArray(cast.animals) && - (cast.animals as unknown[]).every(isAnimal); -} - -function isAnimal(subject: unknown): subject is Animal { - const cast = subject as Animal; - - return typeof cast === "object" && - typeof cast.name === "string" && - isAnimalKind(cast.kind as unknown); -} - -function isAnimalKind(subject: unknown): subject is AnimalKind { - return typeof subject === "string" && includesValue(AnimalKind, subject); -} - -export const validateZoo: Middleware<{ parsedBody: unknown }, { zoo: Zoo }> = - async (req, next) => { - const { parsedBody } = req.context; - - if (!isZoo(parsedBody)) { - return new Response("Invalid Zoo", { status: 422 }); - } - - const nextReq = req.addContext({ zoo: parsedBody }); - - return await next!(nextReq); - }; diff --git a/http/middleware/poc/zoo.ts b/http/middleware/poc/zoo.ts deleted file mode 100644 index f8b0aa8ca234..000000000000 --- a/http/middleware/poc/zoo.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum AnimalKind { - Tiger = "Tiger", - Elephant = "Elephant", - RedPanda = "Red Panda", - Monkey = "Monkey", - Hippo = "Hippo", -} - -export type Animal = { - name: string; - kind: AnimalKind; -}; - -export type Zoo = { - name: string; - entryFee: number; - animals: Animal[]; -}; From 0873130ab0e46299aeb1847ab3f433f717e215dc Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 01:18:22 +0100 Subject: [PATCH 49/84] :memo: Finish middleware docs --- http/README.md | 61 +++++++++++++++++++++++--------------- http/middleware.ts | 6 ++-- http/middleware/example.ts | 6 +++- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/http/README.md b/http/README.md index f4f86aa1b039..8534e89b1724 100644 --- a/http/README.md +++ b/http/README.md @@ -161,8 +161,8 @@ context data. ### Writing Middleware Writing a middleware is the same as writing a `Handler`, except that it gets -passed an additional argument, which is the rest of the chain and should be called -to pass control on. Canonically, that parameter is called `next`. +passed an additional argument, which is the rest of the chain and should be +called to pass control on. Canonically, that parameter is called `next`. To write middleware in typescript, there are two things to decide upfront: @@ -231,23 +231,25 @@ instead of `unknown`, which will allow following code to work with it safely: ```typescript import { Middleware } from "../../../middleware.ts"; -export const validate: Middleware<{ data: unknown }, { data: string[] }> = - (req, next) => { - const { data } = req.context; +export const validate: Middleware<{ data: unknown }, { data: string[] }> = ( + req, + next, +) => { + const { data } = req.context; - if (Array.isArray(data) && data.every((it) => typeof it === "string")) { - const newReq = req.addContext({ - data: data as string[], - }); + if (Array.isArray(data) && data.every((it) => typeof it === "string")) { + const newReq = req.addContext({ + data: data as string[], + }); - return next(newReq); - } + return next(newReq); + } - return new Response( - "Invalid input, expected an array of string", - { status: 422 }, - ); - }; + return new Response( + "Invalid input, expected an array of string", + { status: 422 }, + ); +}; ``` Without explicitly declaring in the `Middleware` type that you depend on a @@ -265,10 +267,14 @@ requirements itself in the right order. An example using the two middleares we wrote above: ```typescript -const handle: Handler<{ data: string }> = (req) => { - console.dir(req.context.data); +const handle: Handler<{ data: string[] }> = (req) => { + const { data } = req.context; - return new Response("Thank you for your strings"); + return new Response( + data + .map((it) => `Hello ${it}!`) + .join("\n"), + ); }; ``` @@ -294,21 +300,24 @@ await listenAndServe(":8000", handleStringArray); ## File Server -A small program for serving local files over HTTP. +There is a small server that serves files from the folder it is running in using +this module: ```sh deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts > HTTP server listening on http://localhost:4507/ ``` -## Cookie +## Cookies -Helpers to manipulate the `Cookie` header. +The module exports some helpers to read and write cookies: ### getCookies +`getCookies` reads cookies from a given `Headers` object: + ```ts -import { getCookies } from "https://deno.land/std@$STD_VERSION/http/cookie.ts"; +import { getCookies } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; const headers = new Headers(); headers.set("Cookie", "full=of; tasty=chocolate"); @@ -319,11 +328,13 @@ console.log(cookies); // { full: "of", tasty: "chocolate" } ### setCookie +`setCookie` will set a cookie on a given `Headers` object: + ```ts import { Cookie, setCookie, -} from "https://deno.land/std@$STD_VERSION/http/cookie.ts"; +} from "https://deno.land/std@$STD_VERSION/http/mod.ts"; const headers = new Headers(); const cookie: Cookie = { name: "Space", value: "Cat" }; @@ -335,6 +346,8 @@ console.log(cookieHeader); // Space=Cat ### deleteCookie +`deleteCookie` will delete a cookie on a given `Headers` object: + > Note: Deleting a `Cookie` will set its expiration date before now. Forcing the > browser to delete it. diff --git a/http/middleware.ts b/http/middleware.ts index 0dcd746c36ee..73438d3e41fd 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,5 +1,5 @@ import type { Handler } from "./server.ts"; -import type { HttpRequest, EmptyContext } from "./request.ts"; +import type { EmptyContext, HttpRequest } from "./request.ts"; import type { CommonKeys, Expand, SafeOmit } from "../_util/types.ts"; /** @@ -49,8 +49,8 @@ export type MiddlewareChain< Adds extends EmptyContext = EmptyContext, > = { ( - req: Parameters>[0], - next?: Parameters>[1], + req: Parameters>[0], + next?: Parameters>[1], ): ReturnType>; /** diff --git a/http/middleware/example.ts b/http/middleware/example.ts index ff8adbc9186c..2416ff7c927f 100644 --- a/http/middleware/example.ts +++ b/http/middleware/example.ts @@ -49,7 +49,11 @@ const validate: Middleware<{ data: unknown }, { data: string[] }> = ( const handle: Handler<{ data: string[] }> = (req) => { const { data } = req.context; - return new Response(data.map((it) => `Hello ${it}`).join("\n")); + return new Response( + data + .map((it) => `Hello ${it}!`) + .join("\n"), + ); }; const stack = chain(log) From d8658e16836c469bfd31b7885adf5d9184f49556 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 01:28:33 +0100 Subject: [PATCH 50/84] :memo: Clarify handlers vs middleware --- http/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/http/README.md b/http/README.md index 8534e89b1724..68f70906ccb6 100644 --- a/http/README.md +++ b/http/README.md @@ -97,12 +97,12 @@ log - authenticate - parseJson - validate - handle <-----------------------------------------Response ``` -Middleware is just code - so it can do anything a normal handler could do, -except that it **can** call the next handler. Middleware will sometimes be used -to ensure that some condition is met before passing the request on (e.g. -authentication, validation), to pre-process requests in some way to make -handling it simpler and less repetitive (deserialization, database preloading) -or to format responses in some way (CORS, compression). +Middleware is just a handler that **can** call the next handler to pass on +control. Middleware will sometimes be used to ensure that some condition is met +before passing the request on (e.g. authentication, validation), to pre-process +requests in some way to make handling it simpler and less repetitive +(deserialization, database preloading) or to format responses in some way (CORS, +compression). `std/http` has a simple, yet powerful, strongly typed middleware system: From 20f107112ea8d6d6afadda6e3ccc087769f95ccd Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 01:54:51 +0100 Subject: [PATCH 51/84] :memo: FIx spelling --- http/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http/README.md b/http/README.md index 68f70906ccb6..fa13c0a4f38b 100644 --- a/http/README.md +++ b/http/README.md @@ -168,7 +168,7 @@ To write middleware in typescript, there are two things to decide upfront: 1. Does your middleware depend on any specific context data of previous middleware? -2. Does your middleware add any data to the context for it's following +2. Does your middleware add any data to the context for its following middleware to consume? Then you write a function using the `Middleware` type, which takes the two @@ -262,7 +262,7 @@ Middleware chains built with the `chain()` function are type safe and order-aware regarding request context, even for arbitrary nesting. This means that Typescript will error if you try to use a chain as a handler for -e.g. `listenAndServe` if that chain does not satisfy all it's internal context +e.g. `listenAndServe` if that chain does not satisfy all its internal context requirements itself in the right order. An example using the two middleares we wrote above: From 1654a818364027d3640effdf67ece2d0b6184819 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 02:35:16 +0100 Subject: [PATCH 52/84] :fire: Remove WIP comments --- http/README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/http/README.md b/http/README.md index fa13c0a4f38b..d2685ae80efa 100644 --- a/http/README.md +++ b/http/README.md @@ -1,14 +1,3 @@ -# Todos - -- Remove Legacy Server - -# PR - -- Wrap Request - - Why - - Have single API - - Allows for cookies, comtext, lazy utilities… - # http Deno's standard HTTP server based on the From 76b4f0f153bcbb41cfafe787557045b94a1672d6 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 02:35:30 +0100 Subject: [PATCH 53/84] :fire: Remove compose export --- http/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/middleware.ts b/http/middleware.ts index 73438d3e41fd..f2e17d4de8c4 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -103,7 +103,7 @@ export type MiddlewareChain< * // This Handler will respond with "Hello Kim" * const handler = composeMiddleware(findUser, hello) * ``` */ -export function composeMiddleware< +function composeMiddleware< FirstNeeds extends EmptyContext, FirstAdd extends EmptyContext, SecondNeeds extends EmptyContext, From 977a9425184a78785159e9cb1e934606a8bfe504 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 02:35:44 +0100 Subject: [PATCH 54/84] :white_check_mark: Add middleware tests --- http/middleware_test.ts | 106 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 http/middleware_test.ts diff --git a/http/middleware_test.ts b/http/middleware_test.ts new file mode 100644 index 000000000000..74b4adb0e881 --- /dev/null +++ b/http/middleware_test.ts @@ -0,0 +1,106 @@ +import { chain, Handler, HttpRequest, Middleware } from "./mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; + +function buildRequest(body?: string) { + return new HttpRequest( + new Request("http://test", { method: "POST", body }), + { + localAddr: { + transport: "tcp", + hostname: "test", + port: 1234, + }, + remoteAddr: { + transport: "tcp", + hostname: "test", + port: 1234, + }, + }, + {}, + ); +} + +Deno.test({ + name: `chain() does not change a handler's behaviour`, + fn: () => { + const handled = new Array(); + + const handler: Handler = async (req) => { + const content = await req.text(); + + handled.push(content); + + return new Response(content); + }; + + const chained = chain(handler); + + const testBody = "foo"; + + handler(buildRequest(testBody)); + chained(buildRequest(testBody)); + + assertEquals(handled[0], handled[1]); + }, +}); + +Deno.test({ + name: `chain() should pass next in the given order`, + fn: () => { + const called = new Array(); + + const first: Middleware = (req, next) => { + called.push(1); + return next(req); + }; + + const second: Middleware = (req, next) => { + called.push(2); + return next(req); + }; + + const handler: Handler = () => new Response(); + const chained = chain(first).add(second).add(handler); + + chained(buildRequest()); + + assertEquals(called, [1, 2]); + }, +}); + +Deno.test({ + name: `nested chains should call depth first`, + fn: () => { + const called = new Array(); + + const first: Middleware = (req, next) => { + called.push(1); + return next(req); + }; + + const second: Middleware = (req, next) => { + called.push(2); + return next(req); + }; + + const third: Middleware = (req, next) => { + called.push(3); + return next(req); + }; + + const fourth: Middleware = (req, next) => { + called.push(4); + return next(req); + }; + + const handler: Handler = () => new Response(); + + const firstChain = chain(first).add(second); + const secondChain = chain(third).add(fourth); + const chained = chain(firstChain).add(secondChain).add(handler); + + chained(buildRequest()); + + assertEquals(called, [1, 2, 3, 4]); + }, +}); From 76cab1fff09697b3e639f31b16754cb2deb2c351 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 02:37:12 +0100 Subject: [PATCH 55/84] :memo: Format --- http/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http/README.md b/http/README.md index d2685ae80efa..34692b939388 100644 --- a/http/README.md +++ b/http/README.md @@ -157,8 +157,8 @@ To write middleware in typescript, there are two things to decide upfront: 1. Does your middleware depend on any specific context data of previous middleware? -2. Does your middleware add any data to the context for its following - middleware to consume? +2. Does your middleware add any data to the context for its following middleware + to consume? Then you write a function using the `Middleware` type, which takes the two points above as optional type arguments, defaulting to the `EmptyContext` (which From b89c67506b741c958ee0b556c95ab0bcb83b3512 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 02:38:12 +0100 Subject: [PATCH 56/84] :art: Change test data --- http/middleware_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http/middleware_test.ts b/http/middleware_test.ts index 74b4adb0e881..10760e0cdd12 100644 --- a/http/middleware_test.ts +++ b/http/middleware_test.ts @@ -7,12 +7,12 @@ function buildRequest(body?: string) { { localAddr: { transport: "tcp", - hostname: "test", + hostname: "test.host", port: 1234, }, remoteAddr: { transport: "tcp", - hostname: "test", + hostname: "test.client", port: 1234, }, }, From 399f32e9c58d0fec27d7fe2b556093d143fed724 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 02:39:50 +0100 Subject: [PATCH 57/84] :art: Add test labels --- http/middleware_test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/http/middleware_test.ts b/http/middleware_test.ts index 10760e0cdd12..7d67d3743faf 100644 --- a/http/middleware_test.ts +++ b/http/middleware_test.ts @@ -21,7 +21,7 @@ function buildRequest(body?: string) { } Deno.test({ - name: `chain() does not change a handler's behaviour`, + name: `[http/middleware] chain() does not change a handler's behaviour`, fn: () => { const handled = new Array(); @@ -45,7 +45,7 @@ Deno.test({ }); Deno.test({ - name: `chain() should pass next in the given order`, + name: `[http/middleware] chain() should pass next in the given order`, fn: () => { const called = new Array(); @@ -69,7 +69,7 @@ Deno.test({ }); Deno.test({ - name: `nested chains should call depth first`, + name: `[http/middleware] nested chains should call depth first`, fn: () => { const called = new Array(); From cb27ad3e7c809ce34eba24fe302e6b79eb9d5ee6 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Sun, 7 Nov 2021 03:23:53 +0100 Subject: [PATCH 58/84] :construction: Add PR draft --- http/middleware/discussion-pr.md | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 http/middleware/discussion-pr.md diff --git a/http/middleware/discussion-pr.md b/http/middleware/discussion-pr.md new file mode 100644 index 000000000000..75cc04979d47 --- /dev/null +++ b/http/middleware/discussion-pr.md @@ -0,0 +1,74 @@ +# `std/http/middleware` + +## Goals + +- Make `std/http` API ergonomic and powerful enough to drive actual applications out of the box + - Similar to most http frameworks, there should be an ergonomic central `req => res` API + offering all information needed in the context of a single request + - Establish minimal but complete middleware pattern that can be used to build out-of-the-box + and third-party middleware in the future to cover very common use cases like CORS, compresison, + deserialization, logging etc. Middleware should be... + - ...just a function, there should be no magic, just a straight callstack + - ...as composable and modularizable as possible + - ...type(script) safe to use and combine, ensuring correct middleware context and order + +## Changes + +This PR contains the following changes: + +### `HttpRequest` Delegate Wrapper + +Adds an `HttpRequest`-class (got a better name? please say so!) that wraps a `Request` object, +delegating to it (and thus implementing the `Request` API interface itself) and adding convenience +methods and properties to have a unified API: + +- A `connInfo` property, holding the `connInfo` that was passed separately before +- A lazy `parsedUrl` property to access a `URL` object for the request url +- A `context` object to hold request context with an `.addContext()` method to add onto it + +Note that there should probably be a lot more convenience methods here - like a (`CookieStore` compatible?) lazy cookie reading +API, maybe a way to access parsed content types lazily etc - I just wanted to create a space where that is possible and clean up +the API without bloating the PR too much. + +### **BREAKING**: `Handler` Signature + +The `Handler` signature has been reduced to just `req: HttpRequest => Response`. + +**This only breaks code that relies on the second `connInfo` argument that was removed**. Code that just depends on the request +will continue to work, as `HttpRequest implements Request`. + +This cleans up the API, creating a place to add more request information onto in `HttpRequest` instead of adding more parameters +while also enabling the established middleware signature in the change below. + +### `Middleware` & `chain()` + +Adds a `chain()` utiltiy that allows to chain `(req: HttpRequest, next: Handler) => Response` middleware together with very little +code in a type safe, order aware manner. Chains can be arbitrarily nested and, given that their request context types are sound, +are directly assignable as `Handler`s: + +```typescript +const handler = chain(parseMiddleware) + .add(validateMiddleware) + .add(dataHandler); + +await listenAndServe(":8000", handler); +``` + +The actual runtime code of `chain()` is very small (like 20 lines in total), but the type safety and resulting DX is made possible +by typing code around the `Middleware` type, which helps users to write middleware by infering the correct types for parameters +as well as forcing middleware authors to explicitly declare their type impact on request context, allowing consumers to get LSP +/ compiler feedback when using their middleware. + +### README Docs + +Adds brief docs on what middleware is in general, how to use it and how to write it. + +Adds brief docs on `HttpRequest`. + +Also expands the other sections a bit, trying to streamline a storyline. + +Removes docs about the legacy server. + +## Try it out + +Under `http/middleware/example/ts` you find a server using the middleware examples from the docs. From 1d758c5925536c518d65ef1499735ce8a705380b Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Tue, 9 Nov 2021 21:48:54 +0100 Subject: [PATCH 59/84] :memo: Add TOC --- http/README.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/http/README.md b/http/README.md index 34692b939388..e4f01b24352b 100644 --- a/http/README.md +++ b/http/README.md @@ -1,8 +1,11 @@ # http Deno's standard HTTP server based on the -[blazing fast native http API](https://deno.land/manual/runtime/http_server_apis#http-server-apis) -under the hood. +[blazing fast native http API](https://deno.land/manual/runtime/http_server_apis#http-server-apis). + +## Table of Contents + +- [Minimal Server](#minimal-server) ## Minimal Server @@ -25,6 +28,7 @@ export type Handler = (request: HttpRequest) => Response | Promise; ``` `std/http` follows web standards, specifically parts of the Fetch API. + `HttpRequest` is an extension of the [`Request` web standard](https://developer.mozilla.org/en-US/docs/Web/API/Request), adding connection information and some helper functions to make it more @@ -32,7 +36,7 @@ convenient to use on servers. The expected return value follows the same Fetch standard and is expected to be a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). -Here is an example handler that echoes the request body back: +Here is an example handler that echoes the request body: ```typescript const handle: Handler = (req) => { @@ -45,7 +49,7 @@ const handle: Handler = (req) => { ## HTTP Status Codes and Methods -The `Status` and `Method` enums offers helper constants for standard HTTP status +The `Status` and `Method` enums offer helper constants for standard HTTP status codes and methods: ```ts @@ -75,8 +79,8 @@ responses like deserialization, compression, validation, CORS etc. A middleware is a special kind of `Handler` that can pass control on to a next handler during its flow, allowing it to only solve a specific part of handling -the request without needing to know about the rest. As the handler it passes -onto can be a middleware itself, this allows to build chains like this: +the request without needing to know about the rest. The next handler can also +be middleware itself, enabling to build chains like this: ``` Request -----------------------------------------> @@ -87,9 +91,11 @@ log - authenticate - parseJson - validate - handle ``` Middleware is just a handler that **can** call the next handler to pass on -control. Middleware will sometimes be used to ensure that some condition is met +control. + +Middleware will sometimes be used to ensure that some condition is met before passing the request on (e.g. authentication, validation), to pre-process -requests in some way to make handling it simpler and less repetitive +requests in some way to make handling them simpler and less repetitive (deserialization, database preloading) or to format responses in some way (CORS, compression). @@ -150,7 +156,7 @@ context data. ### Writing Middleware Writing a middleware is the same as writing a `Handler`, except that it gets -passed an additional argument, which is the rest of the chain and should be +passed an additional argument - the rest of the chain, which should be called to pass control on. Canonically, that parameter is called `next`. To write middleware in typescript, there are two things to decide upfront: From 1f463dd82840988d3b6ef2ea2a2f3cc9b7d78e80 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Tue, 9 Nov 2021 22:52:41 +0100 Subject: [PATCH 60/84] :construction: Some reordering of middleware docs --- http/README.md | 152 ++++++++++++++++++++++++++----------------------- 1 file changed, 81 insertions(+), 71 deletions(-) diff --git a/http/README.md b/http/README.md index e4f01b24352b..85da766f2407 100644 --- a/http/README.md +++ b/http/README.md @@ -6,6 +6,12 @@ Deno's standard HTTP server based on the ## Table of Contents - [Minimal Server](#minimal-server) +- [Handling Requests](#handling-requests) + - [HTTP Status Codes and Methods](#http-status-codes-and-methods) +- [Middleware](#middleware) + - [Writing Simple Middleware]() + - [Request Context]() + - [Using Middleware]() ## Minimal Server @@ -101,82 +107,22 @@ compression). `std/http` has a simple, yet powerful, strongly typed middleware system: -### Using Middleware - -To chain middleware, use the `chain()` function: - -```typescript -import { chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; -import { auth, cors } from "./my_middleware.ts"; - -function sayHello() { - return new Response("Hello"); -} - -const handler = chain(auth) - .add(cors) - .add(sayHello); - -await listenAndServe(":8000", handler); -``` - -This will pass requests through `auth`, which passes on to `cors`, which passes -them on to `sayHello`, with the response from `sayHello` taking the reverse way. - -A chain is itself just a middleware again, so you can pass around and nest -chains as much as you like. This (nonsensical) example does the exact same as -the one above: - -```typescript -import { chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; -import { auth, cors } from "./my_middleware.ts"; - -function sayHello() { - return new Response("Hello"); -} - -const core = chain(cors) - .add(sayHello); - -const handler = chain(auth) - .add(core); - -await listenAndServe(":8000", handler); -``` - -### Request Context - -Request context is a way to pass additional data between middlewares. Each -`HttpRequest`s has an attached `context` object. Arbitrary properties with -arbitrary data can be added to the context via the `.addContext()` method. - -Contexts are very strictly typed to prevent runtime errors due to missing -context data. - ### Writing Middleware -Writing a middleware is the same as writing a `Handler`, except that it gets -passed an additional argument - the rest of the chain, which should be -called to pass control on. Canonically, that parameter is called `next`. - -To write middleware in typescript, there are two things to decide upfront: +Writing middleware is the same as writing a `Handler`, except that it gets +passed an additional argument, canonically called `next`, which is a handler +representing the rest of the chain after this middleware. `std/http` exports +a `Middleware` function type that helps to write a function with the middleware +signature. -1. Does your middleware depend on any specific context data of previous - middleware? -2. Does your middleware add any data to the context for its following middleware - to consume? - -Then you write a function using the `Middleware` type, which takes the two -points above as optional type arguments, defaulting to the `EmptyContext` (which -is an empty object). A simple middleware that logs requests could be written -like this: +Let's look at an example. A simple middleware that logs requests could be written like this: ```typescript import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; export const log: Middleware = async (req, next) => { const start = performance.now(); - const res = await next(req); + const res = await next(req); const duration = performance.now() - start; console.log( @@ -187,11 +133,23 @@ export const log: Middleware = async (req, next) => { }; ``` -Note that because we neither depend on any context data nor add any ourselves, -we can use the bare `Middleware` type here, which is short for -`Middleware`, meaning we depend on the empty context -and add nothing. +This will log a message to stdout for every request, noting how long it took +to respond, the requests method and url and the response's status code. +Note that we choose when or even if we call `next` and what to do with its result. +`next` is just a handler function and as long as our middleware returns a `Response`, +it does not matter how we produced it. + +To write middleware in typescript, there are two things to decide upfront: + +1. Does your middleware depend on any specific context data of previous + middleware? +2. Does your middleware add any data to the context for its following middleware + to consume? + +Then you write a function using the `Middleware` type, which takes the two +points above as optional type arguments, defaulting to the `EmptyContext` (which +is an empty object). A middleware that ensures the incoming payload is yaml and parses it into the request context as `data` for following middleware to consume could be written like this: @@ -251,6 +209,58 @@ Without explicitly declaring in the `Middleware` type that you depend on a certain piece of context data, Typescript will not let you access it on the actual request context object. + +### Using Middleware + +To chain middleware, use the `chain()` function: + +```typescript +import { chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +import { auth, cors } from "./my_middleware.ts"; + +function sayHello() { + return new Response("Hello"); +} + +const handler = chain(auth) + .add(cors) + .add(sayHello); + +await listenAndServe(":8000", handler); +``` + +This will pass requests through `auth`, which passes on to `cors`, which passes +them on to `sayHello`, with the response from `sayHello` taking the reverse way. + +A chain is itself just a middleware again, so you can pass around and nest +chains as much as you like. This (nonsensical) example does the exact same as +the one above: + +```typescript +import { chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +import { auth, cors } from "./my_middleware.ts"; + +function sayHello() { + return new Response("Hello"); +} + +const core = chain(cors) + .add(sayHello); + +const handler = chain(auth) + .add(core); + +await listenAndServe(":8000", handler); +``` + +### Request Context + +Request context is a way to pass additional data between middlewares. Each +`HttpRequest`s has an attached `context` object. Arbitrary properties with +arbitrary data can be added to the context via the `.addContext()` method. + +Contexts are very strictly typed to prevent runtime errors due to missing +context data. ### Chain Type Safety Middleware chains built with the `chain()` function are type safe and From fe0c9b385b1930e321ede66c979b65d136d77ed2 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 10 Nov 2021 22:50:03 +0100 Subject: [PATCH 61/84] :memo: Move Request Context section --- http/README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/http/README.md b/http/README.md index 85da766f2407..adadc55b3dec 100644 --- a/http/README.md +++ b/http/README.md @@ -140,6 +140,18 @@ Note that we choose when or even if we call `next` and what to do with its resul `next` is just a handler function and as long as our middleware returns a `Response`, it does not matter how we produced it. +### Request Context + +Sometimes you want your middleware to pass information onto the handlers after it. +The way to do that is request context. + +Each `HttpRequest`s has an attached `context` object. Arbitrary properties with +arbitrary data can be added to the context via the `.addContext()` method to later +be read by other functions handling the same request. + +Contexts are very strictly typed to prevent runtime errors due to missing +context data. + To write middleware in typescript, there are two things to decide upfront: 1. Does your middleware depend on any specific context data of previous @@ -253,14 +265,6 @@ const handler = chain(auth) await listenAndServe(":8000", handler); ``` -### Request Context - -Request context is a way to pass additional data between middlewares. Each -`HttpRequest`s has an attached `context` object. Arbitrary properties with -arbitrary data can be added to the context via the `.addContext()` method. - -Contexts are very strictly typed to prevent runtime errors due to missing -context data. ### Chain Type Safety Middleware chains built with the `chain()` function are type safe and From 0ad2f51a57ff9d322bfe551e3488b97f420ba7e4 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 10 Nov 2021 23:22:31 +0100 Subject: [PATCH 62/84] :memo: Refactor Middleware context docs --- http/README.md | 119 +++++++++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 48 deletions(-) diff --git a/http/README.md b/http/README.md index adadc55b3dec..ffe9d68038f0 100644 --- a/http/README.md +++ b/http/README.md @@ -7,11 +7,11 @@ Deno's standard HTTP server based on the - [Minimal Server](#minimal-server) - [Handling Requests](#handling-requests) - - [HTTP Status Codes and Methods](#http-status-codes-and-methods) + - [HTTP Status Codes and Methods](#http-status-codes-and-methods) - [Middleware](#middleware) - - [Writing Simple Middleware]() - - [Request Context]() - - [Using Middleware]() + - [Writing Simple Middleware]() + - [Request Context]() + - [Using Middleware]() ## Minimal Server @@ -85,8 +85,8 @@ responses like deserialization, compression, validation, CORS etc. A middleware is a special kind of `Handler` that can pass control on to a next handler during its flow, allowing it to only solve a specific part of handling -the request without needing to know about the rest. The next handler can also -be middleware itself, enabling to build chains like this: +the request without needing to know about the rest. The next handler can also be +middleware itself, enabling to build chains like this: ``` Request -----------------------------------------> @@ -99,30 +99,30 @@ log - authenticate - parseJson - validate - handle Middleware is just a handler that **can** call the next handler to pass on control. -Middleware will sometimes be used to ensure that some condition is met -before passing the request on (e.g. authentication, validation), to pre-process +Middleware will sometimes be used to ensure that some condition is met before +passing the request on (e.g. authentication, validation), to pre-process requests in some way to make handling them simpler and less repetitive (deserialization, database preloading) or to format responses in some way (CORS, compression). -`std/http` has a simple, yet powerful, strongly typed middleware system: +`std/http` has a simple, yet powerful, strongly typed middleware system. ### Writing Middleware Writing middleware is the same as writing a `Handler`, except that it gets -passed an additional argument, canonically called `next`, which is a handler -representing the rest of the chain after this middleware. `std/http` exports -a `Middleware` function type that helps to write a function with the middleware -signature. +passed an additional argument - canonically called `next` - which is a handler +representing the rest of the chain after our middleware. `std/http` exports a +`Middleware` function type that can be used to write Middleware. -Let's look at an example. A simple middleware that logs requests could be written like this: +Let's look at an example. A simple middleware that logs requests could be +written like this: ```typescript import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; export const log: Middleware = async (req, next) => { const start = performance.now(); - const res = await next(req); + const res = await next(req); const duration = performance.now() - start; console.log( @@ -133,43 +133,44 @@ export const log: Middleware = async (req, next) => { }; ``` -This will log a message to stdout for every request, noting how long it took -to respond, the requests method and url and the response's status code. +This will log a message to stdout for every request, noting how long it took to +respond, the requests method and url and the response's status code. -Note that we choose when or even if we call `next` and what to do with its result. -`next` is just a handler function and as long as our middleware returns a `Response`, -it does not matter how we produced it. +Note that we choose when or even if we call `next` and what to do with its +result. `next` is just a handler function and as long as our middleware returns +a `Response`, it does not matter how we produced it. ### Request Context -Sometimes you want your middleware to pass information onto the handlers after it. -The way to do that is request context. +Sometimes you want your middleware to pass information onto the handlers after +it. The way to do that is request context. Each `HttpRequest`s has an attached `context` object. Arbitrary properties with -arbitrary data can be added to the context via the `.addContext()` method to later -be read by other functions handling the same request. +arbitrary data can be added to the context via the `.addContext()` method to +later be read by other functions handling the same request. -Contexts are very strictly typed to prevent runtime errors due to missing -context data. +Contexts are very strictly typed to prevent runtime errors. To write middleware in typescript, there are two things to decide upfront: -1. Does your middleware depend on any specific context data of previous - middleware? -2. Does your middleware add any data to the context for its following middleware - to consume? +### Adding Context -Then you write a function using the `Middleware` type, which takes the two -points above as optional type arguments, defaulting to the `EmptyContext` (which -is an empty object). -A middleware that ensures the incoming payload is yaml and parses it into the -request context as `data` for following middleware to consume could be written -like this: +The `Middleware` type actually takes two optional type arguments: + +- Context of previous middleware we need +- Context we add for handlers after us to consume + +Both default to the `EmptyContext`. Let's look at an example on how to add +context: + +Assume our server wants to accept data in the YAML format. To deal with the YAML +parsing and error handling, we could write a middleware like this: ```typescript import { EmptyContext, Middleware, + Status, } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; import { parse } from "https://deno.land/std@$STD_VERSION/encoding/yaml.ts"; @@ -177,21 +178,39 @@ export const yaml: Middleware = async ( req, next, ) => { - const rawBody = await req.text(); - const data = parse(rawBody); + let data: unknown; + + try { + const rawBody = await req.text(); + data = parse(rawBody); + } catch { + return new Response( + "Could not parse input. Please provide valid YAML", + { status: Status.UnsupportedMediaType }, + ); + } + const newReq = req.addContext({ data }); return await next(newReq); }; ``` -The Typescript compiler will make sure that you actually pass the `data` context -that you decided onto the `next()` handler. +This will respond early with an error if the request does not contain a valid +YAML body and will otherwise parse that YAML, add the parsed Javascript value to +the request context and pass that on to the rest of the chain. + +The Typescript compiler will ensure that we actually pass the promised `data` +property in the context to the `next` handler, because we told it that we will +by setting `Middleware`s second type parameter. + +### Reading Context + +Assume our server only accepts a list of strings as an input and we want to +write a middleware that handles that validation. -Let's write a middleware that will later depend on that `data` property, -validating that it is a list of strings. Note that we also change the context -here, overriding the `data` property to be `string[]` after this middleware -instead of `unknown`, which will allow following code to work with it safely: +To do that, we need to access the previously added `data` property on the +context: ```typescript import { Middleware } from "../../../middleware.ts"; @@ -217,12 +236,16 @@ export const validate: Middleware<{ data: unknown }, { data: string[] }> = ( }; ``` -Without explicitly declaring in the `Middleware` type that you depend on a -certain piece of context data, Typescript will not let you access it on the -actual request context object. +This will check if the parsed `data` value is an Array and if every element in +it is a string. If that is true, it will pass on to the rest of the chain. Note +that we actually change the context - we declare that after our middleware, the +`data` property has the type `string[]` instead of the previous `unknown`. +Without explicitly declaring in the `Middleware` type that we depend on `data` +in the context, Typescript would not have let us access it on the request +context. -### Using Middleware +### Chaining Middleware To chain middleware, use the `chain()` function: From 504601c904e46b6cf5fb9b64b1dfb8dc55a3710d Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 10 Nov 2021 23:39:04 +0100 Subject: [PATCH 63/84] :memo: Work on finishing new middleware section --- http/README.md | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/http/README.md b/http/README.md index ffe9d68038f0..6dc5af418fa5 100644 --- a/http/README.md +++ b/http/README.md @@ -245,23 +245,42 @@ Without explicitly declaring in the `Middleware` type that we depend on `data` in the context, Typescript would not have let us access it on the request context. +This also works with `Handler` - we can and need to tell it which request context +we depend on to use it. Assume that after our middleware above, we want to respond +with a greeting for each string in the provided Array: + +```typescript +import { Handler } from "../../../middleware.ts"; + +export const handleGreetings: Handler<{ data: string[] }> = ( + req, + next, +) => { + const { data } = req.context; + const greetings = data + .map(it => `Hello ${it}!`) + .join("\n"); + + return new Response(greetings); +}; +``` + ### Chaining Middleware -To chain middleware, use the `chain()` function: +How do we actually connect middleware and handlers into a chain? For that, we need +the `chain` function. -```typescript -import { chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; -import { auth, cors } from "./my_middleware.ts"; +Let's do that using our previous examples (assumed to be exported by the `examples.ts` file here): -function sayHello() { - return new Response("Hello"); -} +```typescript +import { chain, serve } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +import { yaml, validate, handleGreeting } from "./examples.ts" -const handler = chain(auth) - .add(cors) - .add(sayHello); +const handler = chain(yaml) + .add(validate) + .add(handleGreeting); -await listenAndServe(":8000", handler); +await serve(handler); ``` This will pass requests through `auth`, which passes on to `cors`, which passes From 39d185afe5e226ca8853410ab889c9c73ff154aa Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 10 Nov 2021 23:39:41 +0100 Subject: [PATCH 64/84] :memo: Work on middleware section --- http/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/http/README.md b/http/README.md index 6dc5af418fa5..a5b9b9da3de7 100644 --- a/http/README.md +++ b/http/README.md @@ -245,9 +245,9 @@ Without explicitly declaring in the `Middleware` type that we depend on `data` in the context, Typescript would not have let us access it on the request context. -This also works with `Handler` - we can and need to tell it which request context -we depend on to use it. Assume that after our middleware above, we want to respond -with a greeting for each string in the provided Array: +This also works with `Handler` - we can and need to tell it which request +context we depend on to use it. Assume that after our middleware above, we want +to respond with a greeting for each string in the provided Array: ```typescript import { Handler } from "../../../middleware.ts"; @@ -258,7 +258,7 @@ export const handleGreetings: Handler<{ data: string[] }> = ( ) => { const { data } = req.context; const greetings = data - .map(it => `Hello ${it}!`) + .map((it) => `Hello ${it}!`) .join("\n"); return new Response(greetings); @@ -267,14 +267,15 @@ export const handleGreetings: Handler<{ data: string[] }> = ( ### Chaining Middleware -How do we actually connect middleware and handlers into a chain? For that, we need -the `chain` function. +How do we actually connect middleware and handlers into a chain? For that, we +need the `chain` function. -Let's do that using our previous examples (assumed to be exported by the `examples.ts` file here): +Let's do that using our previous examples (assumed to be exported by the +`examples.ts` file here): ```typescript import { chain, serve } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; -import { yaml, validate, handleGreeting } from "./examples.ts" +import { handleGreeting, validate, yaml } from "./examples.ts"; const handler = chain(yaml) .add(validate) From 5dc9f039608c19e727e9f79e2727693146050367 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Wed, 10 Nov 2021 23:43:48 +0100 Subject: [PATCH 65/84] :art: Format --- http/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/README.md b/http/README.md index b25e490580c6..6c445696f4fc 100644 --- a/http/README.md +++ b/http/README.md @@ -61,8 +61,8 @@ codes and methods: ```ts import { Handler, - serve, Method, + serve, Status, } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; From bdf4ffc42d78bdfbc0a300fb33c2afb37e94a29d Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 00:56:27 +0100 Subject: [PATCH 66/84] :label: Make handlers terminate chains --- http/middleware.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index f2e17d4de8c4..d96cbdbe88f0 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -50,9 +50,15 @@ export type MiddlewareChain< > = { ( req: Parameters>[0], - next?: Parameters>[1], + next: Parameters>[1], ): ReturnType>; + add( + handler: Handler, + ): Handler< + Expand> + >; + /** * Chain a given middleware to this middleware, returning a new * `MiddlewareChain`. @@ -107,10 +113,10 @@ function composeMiddleware< FirstNeeds extends EmptyContext, FirstAdd extends EmptyContext, SecondNeeds extends EmptyContext, - SecondAdd extends EmptyContext, + SecondAdd extends EmptyContext = EmptyContext, >( first: Middleware, - second: Middleware, + second: Middleware | Handler, ): Middleware< Expand>, Expand> @@ -127,15 +133,25 @@ function composeMiddleware< ); } -/** Wraps the given middleware in a `MiddlewareChain` so it can be `.add()`ed onto */ +export function chain( + handler: Handler, +): Handler; export function chain< Needs extends EmptyContext, Adds extends EmptyContext = EmptyContext, >( middleware: Middleware, -): MiddlewareChain { - const copy = middleware.bind({}) as MiddlewareChain; +): MiddlewareChain; +/** Wraps the given middleware in a `MiddlewareChain` so it can be `.add()`ed onto */ +export function chain< + Needs extends EmptyContext, + Adds extends EmptyContext = EmptyContext, +>( + middleware: Middleware | Handler, +): MiddlewareChain | Handler { + const copy = middleware.bind({}) as typeof middleware; + // @ts-ignore the type is already defined and the implementation is the same copy.add = (m) => chain( composeMiddleware( @@ -144,7 +160,7 @@ export function chain< ), ); - return copy; + return copy as MiddlewareChain | Handler; } type LeftDistinct< From 673fcad301ec7361536a4314ca6764e10932cf63 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 01:17:03 +0100 Subject: [PATCH 67/84] :memo: Adapt documentation to explain terminated chains --- http/README.md | 89 ++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/http/README.md b/http/README.md index 6c445696f4fc..35608d0fd6fb 100644 --- a/http/README.md +++ b/http/README.md @@ -83,10 +83,11 @@ await serve(handle); Middleware is a common pattern to include recurring logic done on requests and responses like deserialization, compression, validation, CORS etc. -A middleware is a special kind of `Handler` that can pass control on to a next -handler during its flow, allowing it to only solve a specific part of handling -the request without needing to know about the rest. The next handler can also be -middleware itself, enabling to build chains like this: +A middleware is like a `Handler` except that it expects an additional parameter +containing a next handler it can pass on to, allowing it to only solve a +specific part of handling the request without needing to know about the rest. +The next handler can contain middleware again, enabling to build chains like +this: ``` Request -----------------------------------------> @@ -96,14 +97,15 @@ log - authenticate - parseJson - validate - handle <-----------------------------------------Response ``` -Middleware is just a handler that **can** call the next handler to pass on -control. +Middleware is a handler that **can** call the next handler to pass on control. -Middleware will sometimes be used to ensure that some condition is met before +Middleware will often be used to ensure that some condition is met before passing the request on (e.g. authentication, validation), to pre-process requests in some way to make handling them simpler and less repetitive (deserialization, database preloading) or to format responses in some way (CORS, -compression). +compression). The middleware pattern decouples those parts of the logic from the +rest of the application, allowing to distribute it as reusable modules, even +from third parties. `std/http` has a simple, yet powerful, strongly typed middleware system. @@ -112,7 +114,7 @@ compression). Writing middleware is the same as writing a `Handler`, except that it gets passed an additional argument - canonically called `next` - which is a handler representing the rest of the chain after our middleware. `std/http` exports a -`Middleware` function type that can be used to write Middleware. +`Middleware` function type that should be used to write Middleware. Let's look at an example. A simple middleware that logs requests could be written like this: @@ -142,7 +144,7 @@ a `Response`, it does not matter how we produced it. ### Request Context -Sometimes you want your middleware to pass information onto the handlers after +Sometimes you want your middleware to pass information onto the handler after it. The way to do that is request context. Each `HttpRequest`s has an attached `context` object. Arbitrary properties with @@ -151,8 +153,6 @@ later be read by other functions handling the same request. Contexts are very strictly typed to prevent runtime errors. -To write middleware in typescript, there are two things to decide upfront: - ### Adding Context The `Middleware` type actually takes two optional type arguments: @@ -161,7 +161,7 @@ The `Middleware` type actually takes two optional type arguments: - Context we add for handlers after us to consume Both default to the `EmptyContext`. Let's look at an example on how to add -context: +context. Assume our server wants to accept data in the YAML format. To deal with the YAML parsing and error handling, we could write a middleware like this: @@ -198,7 +198,7 @@ export const yaml: Middleware = async ( This will respond early with an error if the request does not contain a valid YAML body and will otherwise parse that YAML, add the parsed Javascript value to -the request context and pass that on to the rest of the chain. +the request context and pass that on to the next handler. The Typescript compiler will ensure that we actually pass the promised `data` property in the context to the `next` handler, because we told it that we will @@ -237,9 +237,10 @@ export const validate: Middleware<{ data: unknown }, { data: string[] }> = ( ``` This will check if the parsed `data` value is an Array and if every element in -it is a string. If that is true, it will pass on to the rest of the chain. Note -that we actually change the context - we declare that after our middleware, the -`data` property has the type `string[]` instead of the previous `unknown`. +it is a string. If that is true, it will pass on to the next handler. Note that +we actually change the context - we declare (in the second type parameter of +`Middleware`) that after our middleware, the `data` property has the type +`string[]` instead of the previous `unknown`. Without explicitly declaring in the `Middleware` type that we depend on `data` in the context, Typescript would not have let us access it on the request @@ -252,10 +253,7 @@ to respond with a greeting for each string in the provided Array: ```typescript import { Handler } from "../../../middleware.ts"; -export const handleGreetings: Handler<{ data: string[] }> = ( - req, - next, -) => { +export const handleGreetings: Handler<{ data: string[] }> = (req) => { const { data } = req.context; const greetings = data .map((it) => `Hello ${it}!`) @@ -265,6 +263,9 @@ export const handleGreetings: Handler<{ data: string[] }> = ( }; ``` +If we would not have explicitly added `{ data: string[] }` to `Handler`, +Typescript would not have let us access that context. + ### Chaining Middleware How do we actually connect middleware and handlers into a chain? For that, we @@ -274,49 +275,51 @@ Let's do that using our previous examples (assumed to be exported by the `examples.ts` file here): ```typescript -import { chain, serve } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; -import { handleGreeting, validate, yaml } from "./examples.ts"; +import { chain, serve } from "https://deno.land/std@$std_version/http/mod.ts"; +import { handlegreeting, validate, yaml } from "./examples.ts"; const handler = chain(yaml) .add(validate) - .add(handleGreeting); + .add(handlegreeting); await serve(handler); ``` -This will pass requests through `auth`, which passes on to `cors`, which passes -them on to `sayHello`, with the response from `sayHello` taking the reverse way. +This will build a new function by chaining the given functions in the given +order. You can `.add()` as many middlewares as you want until you `.add()` a +`Handler`, which will terminate the chain, turning it into a `Handler` itself, +meaning you can no longer add to it. -A chain is itself just a middleware again, so you can pass around and nest -chains as much as you like. This (nonsensical) example does the exact same as -the one above: +### Chain Nesting -```typescript -import { chain, serve } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; -import { auth, cors } from "./my_middleware.ts"; +Chains are just middlewares (or `Handler`s if they are terminated) again, so you +can pass around and nest them in other chains as much and as deeply as you want. -function sayHello() { - return new Response("Hello"); -} +This example does the exact same as the one above: -const core = chain(cors) - .add(sayHello); +```typescript +import { chain, serve } from "https://deno.land/std@$std_version/http/mod.ts"; +import { handlegreeting, validate, yaml } from "./examples.ts"; -const handler = chain(auth) - .add(core); +const validateAndGreet = chain(validate) + .add(handleGreeting); + +const handler = chain(yaml) + .add(valideAndGreet); await serve(handler); ``` ### Chain Type Safety -Middleware chains built with the `chain()` function are type safe and -order-aware regarding request context, even for arbitrary nesting. +Chains built with the `chain()` function are type safe and order-aware regarding +request context, even for arbitrary nesting. This means that Typescript will error if you try to use a chain as a handler for e.g. `serve` if that chain does not satisfy all its internal context -requirements itself in the right order. An example using the two middleares we -wrote above: +requirements itself in the right order. + +Let's use our examples from above again to demonstrate this: ```typescript const handle: Handler<{ data: string[] }> = (req) => { From 2c9c1d9bcd99561683e2c79806624306066b81dc Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 01:25:57 +0100 Subject: [PATCH 68/84] :memo: Finish seocnd veriosn of docs --- http/README.md | 52 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/http/README.md b/http/README.md index 35608d0fd6fb..7137394bd5b4 100644 --- a/http/README.md +++ b/http/README.md @@ -9,9 +9,18 @@ Deno's standard HTTP server based on the - [Handling Requests](#handling-requests) - [HTTP Status Codes and Methods](#http-status-codes-and-methods) - [Middleware](#middleware) - - [Writing Simple Middleware]() - - [Request Context]() - - [Using Middleware]() + - [Writing Middleware]() + - [Request Context](#request-context) + - [Adding Context](#adding-context) + - [Reading Context](#reading-context) + - [Chaining Middleware](#chaining-middleware) + - [Nesting Chains](#nesting-chains) + - [Chain Type Safety](#chain-type-safety) +- [File Server](#file-server) +- [Cookies](#cookies) + - [getCookies](#getcookies) + - [setCookie](#setcookie) + - [deleteCookie]($deletecookie) ## Minimal Server @@ -290,7 +299,7 @@ order. You can `.add()` as many middlewares as you want until you `.add()` a `Handler`, which will terminate the chain, turning it into a `Handler` itself, meaning you can no longer add to it. -### Chain Nesting +### Nesting Chains Chains are just middlewares (or `Handler`s if they are terminated) again, so you can pass around and nest them in other chains as much and as deeply as you want. @@ -319,31 +328,22 @@ This means that Typescript will error if you try to use a chain as a handler for e.g. `serve` if that chain does not satisfy all its internal context requirements itself in the right order. -Let's use our examples from above again to demonstrate this: +Let's use our examples from above again to demonstrate this. This will not pass +the type checker: ```typescript -const handle: Handler<{ data: string[] }> = (req) => { - const { data } = req.context; - - return new Response( - data - .map((it) => `Hello ${it}!`) - .join("\n"), - ); -}; -``` - -This will not pass the type checker: +import { chain, serve } from "https://deno.land/std@$std_version/http/mod.ts"; +import { handlegreeting, validate, yaml } from "./examples.ts"; -```typescript const handleStringArray = chain(validate) .add(yaml) .add(handle); +// TS will correctly tell you that this Handler requires { data: unknown } context, which `serve` will not provide await serve(handleStringArray); ``` -But this will: +But this will work, with the only difference being the order: ```typescript const handleStringArray = chain(yaml) @@ -353,6 +353,20 @@ const handleStringArray = chain(yaml) await serve(handleStringArray); ``` +As `serve` only accepts `Handler`, Typescript will also stop you from passing it +an unterminated chain: + +```typescript +const handleStringArray = chain(yaml) + .add(validate); + +// TS will tell you that the MiddlewareChain is not assignable to the Handler here +await serve(handleStringArray); +``` + +Those checks will help you not to run into runtime problems, even for more +complex, nested setups. + ## File Server There is a small server that serves files from the folder it is running in using From eb9667e7f94f7993701e14841ddd6fc573591797 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 01:27:50 +0100 Subject: [PATCH 69/84] :memo: Update middleware example --- http/middleware/example.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/http/middleware/example.ts b/http/middleware/example.ts index 2416ff7c927f..380fc60ca275 100644 --- a/http/middleware/example.ts +++ b/http/middleware/example.ts @@ -1,10 +1,4 @@ -import { - chain, - EmptyContext, - Handler, - listenAndServe, - Middleware, -} from "../mod.ts"; +import { chain, EmptyContext, Handler, Middleware, serve } from "../mod.ts"; import { parse } from "../../encoding/yaml.ts"; const log: Middleware = async (req, next) => { @@ -56,9 +50,9 @@ const handle: Handler<{ data: string[] }> = (req) => { ); }; -const stack = chain(log) +const handler = chain(log) .add(yaml) .add(validate) .add(handle); -await listenAndServe(":8000", stack); +await serve(handler); From b82773eb82987ee9a817059447ffed8a330e76f2 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 01:29:59 +0100 Subject: [PATCH 70/84] :memo: Fix TOC in http readme --- http/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http/README.md b/http/README.md index 7137394bd5b4..4f39c441f3ad 100644 --- a/http/README.md +++ b/http/README.md @@ -9,7 +9,7 @@ Deno's standard HTTP server based on the - [Handling Requests](#handling-requests) - [HTTP Status Codes and Methods](#http-status-codes-and-methods) - [Middleware](#middleware) - - [Writing Middleware]() + - [Writing Middleware](#writing-middleware) - [Request Context](#request-context) - [Adding Context](#adding-context) - [Reading Context](#reading-context) @@ -20,7 +20,7 @@ Deno's standard HTTP server based on the - [Cookies](#cookies) - [getCookies](#getcookies) - [setCookie](#setcookie) - - [deleteCookie]($deletecookie) + - [deleteCookie](#deletecookie) ## Minimal Server From d2d86686d2f08164d9b0f9830574745bb68c781f Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 10:40:38 +0100 Subject: [PATCH 71/84] :speech_balloon: Draft PR --- http/middleware/discussion-pr.md | 76 +++++++++++++++++++------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/http/middleware/discussion-pr.md b/http/middleware/discussion-pr.md index 75cc04979d47..4f7f47e16a45 100644 --- a/http/middleware/discussion-pr.md +++ b/http/middleware/discussion-pr.md @@ -2,15 +2,19 @@ ## Goals -- Make `std/http` API ergonomic and powerful enough to drive actual applications out of the box - - Similar to most http frameworks, there should be an ergonomic central `req => res` API - offering all information needed in the context of a single request - - Establish minimal but complete middleware pattern that can be used to build out-of-the-box - and third-party middleware in the future to cover very common use cases like CORS, compresison, - deserialization, logging etc. Middleware should be... +- Make `std/http` API ergonomic and powerful enough to drive actual applications + out of the box + - Similar to most http frameworks, there should be an ergonomic central + `req => res` API offering all information needed in the context of a single + request + - Establish minimal but complete middleware pattern that can be used to build + out-of-the-box and third-party middleware in the future to cover very common + use cases like CORS, compresison, deserialization, logging etc. Middleware + should be... - ...just a function, there should be no magic, just a straight callstack - ...as composable and modularizable as possible - - ...type(script) safe to use and combine, ensuring correct middleware context and order + - ...type(script) safe to use and combine, ensuring correct middleware + context and order ## Changes @@ -18,50 +22,61 @@ This PR contains the following changes: ### `HttpRequest` Delegate Wrapper -Adds an `HttpRequest`-class (got a better name? please say so!) that wraps a `Request` object, -delegating to it (and thus implementing the `Request` API interface itself) and adding convenience -methods and properties to have a unified API: +Adds an `HttpRequest`-class (got a better name? please say so!) that wraps a +`Request` object, delegating to it (and thus implementing the `Request` API +interface itself) and adding convenience methods and properties to have a +unified API: -- A `connInfo` property, holding the `connInfo` that was passed separately before +- A `connInfo` property, holding the `connInfo` that was passed separately + before - A lazy `parsedUrl` property to access a `URL` object for the request url -- A `context` object to hold request context with an `.addContext()` method to add onto it +- A `context` object to hold request context with an `.addContext()` method to + add onto it -Note that there should probably be a lot more convenience methods here - like a (`CookieStore` compatible?) lazy cookie reading -API, maybe a way to access parsed content types lazily etc - I just wanted to create a space where that is possible and clean up -the API without bloating the PR too much. +Note that there should probably be a lot more convenience methods here - like a +(`CookieStore` compatible?) lazy cookie reading API, maybe a way to access +parsed content types lazily etc - I just wanted to create a space where that is +possible and clean up the API without bloating the PR too much. ### **BREAKING**: `Handler` Signature The `Handler` signature has been reduced to just `req: HttpRequest => Response`. -**This only breaks code that relies on the second `connInfo` argument that was removed**. Code that just depends on the request -will continue to work, as `HttpRequest implements Request`. +**This only breaks code that relies on the second `connInfo` argument that was +removed**. Code that just depends on the request will continue to work, as +`HttpRequest implements Request`. -This cleans up the API, creating a place to add more request information onto in `HttpRequest` instead of adding more parameters -while also enabling the established middleware signature in the change below. +This cleans up the API, creating a place to add more request information onto in +`HttpRequest` instead of adding more parameters while also enabling the +established middleware signature in the change below. ### `Middleware` & `chain()` -Adds a `chain()` utiltiy that allows to chain `(req: HttpRequest, next: Handler) => Response` middleware together with very little -code in a type safe, order aware manner. Chains can be arbitrarily nested and, given that their request context types are sound, -are directly assignable as `Handler`s: +Adds a `chain()` utiltiy that allows to chain +`(req: HttpRequest, next: Handler) => Response` middleware together with very +little code in a type safe, order aware manner. Chains can be arbitrarily nested +and, given that their request context types are sound, are directly assignable +as `Handler`s: ```typescript const handler = chain(parseMiddleware) - .add(validateMiddleware) - .add(dataHandler); + .add(validateMiddleware) + .add(dataHandler); await listenAndServe(":8000", handler); ``` -The actual runtime code of `chain()` is very small (like 20 lines in total), but the type safety and resulting DX is made possible -by typing code around the `Middleware` type, which helps users to write middleware by infering the correct types for parameters -as well as forcing middleware authors to explicitly declare their type impact on request context, allowing consumers to get LSP -/ compiler feedback when using their middleware. +The actual runtime code of `chain()` is very small (like 20 lines in total), but +the type safety and resulting DX is made possible by typing code around the +`Middleware` type, which helps users to write middleware by infering the correct +types for parameters as well as forcing middleware authors to explicitly declare +their type impact on request context, allowing consumers to get LSP / compiler +feedback when using their middleware. ### README Docs -Adds brief docs on what middleware is in general, how to use it and how to write it. +Adds brief docs on what middleware is in general, how to use it and how to write +it. Adds brief docs on `HttpRequest`. @@ -71,4 +86,5 @@ Removes docs about the legacy server. ## Try it out -Under `http/middleware/example/ts` you find a server using the middleware examples from the docs. +Under `http/middleware/example/ts` you find a server using the middleware +examples from the docs. From 1ed536f7f1a36d84672358b3de51e8e136324a1e Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 14:42:19 +0100 Subject: [PATCH 72/84] :memo: Another pass on doc language, add Error Handling section --- http/README.md | 164 +++++++++++++++++++++++++++++++------------------ 1 file changed, 104 insertions(+), 60 deletions(-) diff --git a/http/README.md b/http/README.md index 4f39c441f3ad..b3ebbad44a3e 100644 --- a/http/README.md +++ b/http/README.md @@ -5,7 +5,7 @@ Deno's standard HTTP server based on the ## Table of Contents -- [Minimal Server](#minimal-server) +- [Minimal Server Example](#minimal-server-example) - [Handling Requests](#handling-requests) - [HTTP Status Codes and Methods](#http-status-codes-and-methods) - [Middleware](#middleware) @@ -16,15 +16,17 @@ Deno's standard HTTP server based on the - [Chaining Middleware](#chaining-middleware) - [Nesting Chains](#nesting-chains) - [Chain Type Safety](#chain-type-safety) + - [Error Handling](#error-handling) - [File Server](#file-server) - [Cookies](#cookies) - [getCookies](#getcookies) - [setCookie](#setcookie) - [deleteCookie](#deletecookie) -## Minimal Server +## Minimal Server Example -Run this file with `--allow-net` and try requesting `http://localhost:8000`: +Run this file with `--allow-net` and try requesting `http://localhost:8000` +(default port if no second argument is supplied to `serve`): ```ts import { serve } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; @@ -42,14 +44,12 @@ and returns a [`Response`](https://doc.deno.land/builtin/stable#Response): export type Handler = (request: HttpRequest) => Response | Promise; ``` -`std/http` follows web standards, specifically parts of the Fetch API. - -`HttpRequest` is an extension of the -[`Request` web standard](https://developer.mozilla.org/en-US/docs/Web/API/Request), +`std/http` follows web standards, specifically parts of the Fetch API, with +`HttpRequest` being an extension of the +[`Request` standard](https://developer.mozilla.org/en-US/docs/Web/API/Request), adding connection information and some helper functions to make it more -convenient to use on servers. The expected return value follows the same Fetch -standard and is expected to be a -[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). +convenient to use on servers, and the expected response value being a +[standard `Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). Here is an example handler that echoes the request body: @@ -57,6 +57,7 @@ Here is an example handler that echoes the request body: const handle: Handler = (req) => { return new Response( req.body, + // This is how you explicitly set the response status code - 200 is the default { status: 200 }, ); }; @@ -89,8 +90,8 @@ await serve(handle); ## Middleware -Middleware is a common pattern to include recurring logic done on requests and -responses like deserialization, compression, validation, CORS etc. +Middleware is a common pattern to modularize recurring logic done on requests +and responses like deserialization, compression, validation, CORS etc. A middleware is like a `Handler` except that it expects an additional parameter containing a next handler it can pass on to, allowing it to only solve a @@ -99,30 +100,27 @@ The next handler can contain middleware again, enabling to build chains like this: ``` -Request -----------------------------------------> +Request --------------------------> -log - authenticate - parseJson - validate - handle +log - parseYaml - validate - handle -<-----------------------------------------Response +<------------------------- Response ``` -Middleware is a handler that **can** call the next handler to pass on control. - Middleware will often be used to ensure that some condition is met before passing the request on (e.g. authentication, validation), to pre-process requests in some way to make handling them simpler and less repetitive (deserialization, database preloading) or to format responses in some way (CORS, -compression). The middleware pattern decouples those parts of the logic from the -rest of the application, allowing to distribute it as reusable modules, even -from third parties. +compression). The middleware pattern decouples those parts from the rest of the +handling logic, allowing to distribute middleware as reusable modules. -`std/http` has a simple, yet powerful, strongly typed middleware system. +`std/http` has a simple, yet powerful, Typescript-native middleware system. ### Writing Middleware Writing middleware is the same as writing a `Handler`, except that it gets passed an additional argument - canonically called `next` - which is a handler -representing the rest of the chain after our middleware. `std/http` exports a +representing the rest of the chain _after_ our middleware. `std/http` exports a `Middleware` function type that should be used to write Middleware. Let's look at an example. A simple middleware that logs requests could be @@ -154,13 +152,15 @@ a `Response`, it does not matter how we produced it. ### Request Context Sometimes you want your middleware to pass information onto the handler after -it. The way to do that is request context. +it. As middleware is meant to be modular and independently composable, the way +to do that is request context. -Each `HttpRequest`s has an attached `context` object. Arbitrary properties with -arbitrary data can be added to the context via the `.addContext()` method to -later be read by other functions handling the same request. +Each `HttpRequest`s has an attached `context` object, which starts out emty. +Arbitrary properties with arbitrary data can be added to the context via the +`.addContext()` method to later be read by other functions handling the same +request. -Contexts are very strictly typed to prevent runtime errors. +Request contexts are very strictly typed to prevent runtime errors. ### Adding Context @@ -172,8 +172,8 @@ The `Middleware` type actually takes two optional type arguments: Both default to the `EmptyContext`. Let's look at an example on how to add context. -Assume our server wants to accept data in the YAML format. To deal with the YAML -parsing and error handling, we could write a middleware like this: +Assume our server wants to accept data in the YAML format. To deal with YAML +parsing and handling parsing errors, we could write a middleware like this: ```typescript import { @@ -183,6 +183,7 @@ import { } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; import { parse } from "https://deno.land/std@$STD_VERSION/encoding/yaml.ts"; +// We depend on no context data, but we add a `data` property with the type `unknown` export const yaml: Middleware = async ( req, next, @@ -194,11 +195,12 @@ export const yaml: Middleware = async ( data = parse(rawBody); } catch { return new Response( - "Could not parse input. Please provide valid YAML", + "Could not parse input - please provide valid YAML", { status: Status.UnsupportedMediaType }, ); } + // addContext() returns a new request reference with the new context type const newReq = req.addContext({ data }); return await next(newReq); @@ -222,8 +224,9 @@ To do that, we need to access the previously added `data` property on the context: ```typescript -import { Middleware } from "../../../middleware.ts"; +import { Middleware, Status } from "../../../middleware.ts"; +// We depend on a `data: unkown` context and we will add a `data: string[]` context export const validate: Middleware<{ data: unknown }, { data: string[] }> = ( req, next, @@ -239,29 +242,32 @@ export const validate: Middleware<{ data: unknown }, { data: string[] }> = ( } return new Response( - "Invalid input, expected an array of string", - { status: 422 }, + "Invalid input, expected an array of strings", + { status: Status.UnprocessableEntity }, ); }; ``` -This will check if the parsed `data` value is an Array and if every element in -it is a string. If that is true, it will pass on to the next handler. Note that -we actually change the context - we declare (in the second type parameter of -`Middleware`) that after our middleware, the `data` property has the type -`string[]` instead of the previous `unknown`. +This will access the `data` value on the context, check if it is an Array and if +every element in it is a string. If that is true, it will pass on to the next +handler. Note that we actually change the context as well - we declare (in the +second type parameter of `Middleware`) that after our middleware, the `data` +property has the type `string[]` instead of the previous `unknown`. We could +have added a new property as well, but now that we know `data`s type, it does +not make a lot of sense to keep the `unknown` property. Without explicitly declaring in the `Middleware` type that we depend on `data` -in the context, Typescript would not have let us access it on the request -context. +in the context, Typescript would not have let us access it. -This also works with `Handler` - we can and need to tell it which request -context we depend on to use it. Assume that after our middleware above, we want -to respond with a greeting for each string in the provided Array: +`Handler`s read context the same way - we can and need to tell the `Handler` +type which request context we depend on to access it. Assume that after our +middleware above, we want to respond with a greeting for each string in the +provided Array: ```typescript import { Handler } from "../../../middleware.ts"; +// We explicitly declare that we need a `data: string[]` context export const handleGreetings: Handler<{ data: string[] }> = (req) => { const { data } = req.context; const greetings = data @@ -272,26 +278,26 @@ export const handleGreetings: Handler<{ data: string[] }> = (req) => { }; ``` -If we would not have explicitly added `{ data: string[] }` to `Handler`, -Typescript would not have let us access that context. +If we would not have explicitly passed `{ data: string[] }` to the `Handler` +type, Typescript would not have let us access that context. ### Chaining Middleware How do we actually connect middleware and handlers into a chain? For that, we -need the `chain` function. +need the `chain()` function. Let's do that using our previous examples (assumed to be exported by the -`examples.ts` file here): +`./examples.ts` file here): ```typescript import { chain, serve } from "https://deno.land/std@$std_version/http/mod.ts"; -import { handlegreeting, validate, yaml } from "./examples.ts"; +import { handleGreetings, validate, yaml } from "./examples.ts"; -const handler = chain(yaml) +const serverHandler = chain(yaml) .add(validate) - .add(handlegreeting); + .add(handleGreetings); -await serve(handler); +await serve(serverHandler); ``` This will build a new function by chaining the given functions in the given @@ -301,22 +307,23 @@ meaning you can no longer add to it. ### Nesting Chains -Chains are just middlewares (or `Handler`s if they are terminated) again, so you -can pass around and nest them in other chains as much and as deeply as you want. +Chains are just `Middleware`s (or `Handler`s if they are terminated) themself, +so you can pass around and nest them in other chains as much and as deeply as +you want. This example does the exact same as the one above: ```typescript import { chain, serve } from "https://deno.land/std@$std_version/http/mod.ts"; -import { handlegreeting, validate, yaml } from "./examples.ts"; +import { handleGreetings, validate, yaml } from "./examples.ts"; const validateAndGreet = chain(validate) - .add(handleGreeting); + .add(handleGreetings); -const handler = chain(yaml) +const serverHandler = chain(yaml) .add(valideAndGreet); -await serve(handler); +await serve(serverHandler); ``` ### Chain Type Safety @@ -343,7 +350,8 @@ const handleStringArray = chain(validate) await serve(handleStringArray); ``` -But this will work, with the only difference being the order: +But this will work - note that the only difference to the eaxmple above is the +order: ```typescript const handleStringArray = chain(yaml) @@ -353,8 +361,8 @@ const handleStringArray = chain(yaml) await serve(handleStringArray); ``` -As `serve` only accepts `Handler`, Typescript will also stop you from passing it -an unterminated chain: +As `serve` only accepts a `Handler`, Typescript will also stop you from passing +it an unterminated chain: ```typescript const handleStringArray = chain(yaml) @@ -367,6 +375,42 @@ await serve(handleStringArray); Those checks will help you not to run into runtime problems, even for more complex, nested setups. +### Error Handling + +Chains are really just functions - which means that a request passing thrugh a +chain is just passing through a normal call stack. + +This means that you can handle `Error`s exactly as you would with any other +normal nested functions, with `Error`s bubbling up the middleware chain until +they are caught (which `serve` will do if no other function did it before). + +So you could easily write custom `Error` for downstream functions to use and +handle it in your middleware: + +```typescript +import { + Middleware, + Status, +} from "https://deno.land/std@$std_version/http/mod.ts"; + +export class AuthorizationError extends Error {} + +export const authError: Middleware = async (req, next) => { + try { + return await next(req); + } catch (e) { + if (e instanceof AuthorizationError) { + return new Response( + null, + { status: Status.Forbidden }, + ); + } + + throw e; + } +}; +``` + ## File Server There is a small server that serves files from the folder it is running in using From 5ae1df7504d359dc0e9923586482e91e6059eae2 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 14:44:34 +0100 Subject: [PATCH 73/84] :memo: Formatting and link to example file --- http/README.md | 3 +++ http/middleware/example.ts | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/http/README.md b/http/README.md index b3ebbad44a3e..162d9970f5e4 100644 --- a/http/README.md +++ b/http/README.md @@ -305,6 +305,9 @@ order. You can `.add()` as many middlewares as you want until you `.add()` a `Handler`, which will terminate the chain, turning it into a `Handler` itself, meaning you can no longer add to it. +All examples up to this point can be found in +[`middleware/example.ts`](middleware/example.ts). + ### Nesting Chains Chains are just `Middleware`s (or `Handler`s if they are terminated) themself, diff --git a/http/middleware/example.ts b/http/middleware/example.ts index 380fc60ca275..c46295b8e3a4 100644 --- a/http/middleware/example.ts +++ b/http/middleware/example.ts @@ -1,4 +1,11 @@ -import { chain, EmptyContext, Handler, Middleware, serve } from "../mod.ts"; +import { + chain, + EmptyContext, + Handler, + Middleware, + serve, + Status, +} from "../mod.ts"; import { parse } from "../../encoding/yaml.ts"; const log: Middleware = async (req, next) => { @@ -36,7 +43,7 @@ const validate: Middleware<{ data: unknown }, { data: string[] }> = ( return new Response( "Invalid input, expected an array of string", - { status: 422 }, + { status: Status.UnprocessableEntity }, ); }; From 5d0f0d784bc5dec3417f89dc380b5a00afe97577 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 14:59:19 +0100 Subject: [PATCH 74/84] :memo: Update middleware JSDoc to reflect recent changes --- http/middleware.ts | 78 +++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/http/middleware.ts b/http/middleware.ts index d96cbdbe88f0..b04bd8a533eb 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -3,11 +3,11 @@ import type { EmptyContext, HttpRequest } from "./request.ts"; import type { CommonKeys, Expand, SafeOmit } from "../_util/types.ts"; /** -* Middleware Handler that is expected to call the `next` Middleware at some -* point, adding its own logic, request context and transformations befor or -* after it. Can express the request context (see `HttpRequest.context`) it -* requires to work and any request context extensions it will add to be -* available to the `next` middleware. +* Middleware to deal with some part of handling a request. It will be paseed a +* `next` handler that it can call at any point point, adding its own logic, +* request context and transformations befor or after it. Can express the +* request context (see `HttpRequest.context`) it requires to work and any +* request context it will add to be available to the `next` handler. * * @typeParam Needs - Request context required by this Middleware, defaults to * `EmptyContext` which is the empty object @@ -22,7 +22,7 @@ import type { CommonKeys, Expand, SafeOmit } from "../_util/types.ts"; * // Log response time, method and path for requests going through this middleware * const log: Middleware = (req, next) => { * const start = performance.now(); -* const response = await next!(req); +* const response = await next(req); * const end = performance.now(); * console.log( @@ -42,8 +42,9 @@ export type Middleware< ) => Response | Promise; /** - * A `Middleware` that can be chained onto with `.add()`. Use `chain()` to wrap - * a `Middleware` as a `MiddlewareChain`. */ + * A `Middleware` that can be chained onto with `.add()`. Use `chain()` to start + * a `MiddlewareChain`. + */ export type MiddlewareChain< Needs extends EmptyContext, Adds extends EmptyContext = EmptyContext, @@ -53,6 +54,30 @@ export type MiddlewareChain< next: Parameters>[1], ): ReturnType>; + /** + * Append a given handler to this chain, terminating it, returning a new handler. + * + * Example: + * + * ```ts + * const first: Middleware = (req, next) => { + * console.log("Hey"); + * return await next(req); + * }; + * + * const second: Middleware = (req, next) => { + * console.log("there!"); + * return await next(req); + * }; + * + * const helloWorld = (req) => new Response("Hello world"); + * + * // This Handler will log "Hey", log "there!" and then respond with "Hello World" + * const handler = chain(first) + * .add(second) + * .add(helloWorld); + * ``` + */ add( handler: Handler, ): Handler< @@ -60,7 +85,7 @@ export type MiddlewareChain< >; /** - * Chain a given middleware to this middleware, returning a new + * Append a given middleware to this chain, returning a new * `MiddlewareChain`. * * Example: @@ -68,20 +93,17 @@ export type MiddlewareChain< * ```ts * const first: Middleware = (req, next) => { * console.log("Hey"); - * return await next!(req); + * return await next(req); * }; * * const second: Middleware = (req, next) => { * console.log("there!"); - * return await next!(req); + * return await next(req); * }; * - * const helloWorld = (req) => new Response("Hello world"); - * - * // This Handler will log "Hey", log "there!" and then respond with "Hello World" - * const handler = chain(first) - * .add(second) - * .add(helloWorld) + * // This middleware will log "Hey", log "there!" and pass on to whatever is appended to it + * const middleware = chain(first) + * .add(second); * ``` */ add( @@ -92,23 +114,6 @@ export type MiddlewareChain< >; }; -/** - * Builds a new `Middleware` out of two given ones, chaining them. - * - * Example: - * - * ```ts - * const findUser: Middleware = (req, next) => { - * return await next!( - * req.addContext({ user: "Kim" }); - * ); - * }; - * - * const hello = (req: HttpRequest<{ user: string }>) => new Response(`Hello ${req.context.user}`); - * - * // This Handler will respond with "Hello Kim" - * const handler = composeMiddleware(findUser, hello) - * ``` */ function composeMiddleware< FirstNeeds extends EmptyContext, FirstAdd extends EmptyContext, @@ -133,16 +138,19 @@ function composeMiddleware< ); } +/** Wraps the given handler, doing nothing and returning a copy */ export function chain( handler: Handler, ): Handler; + +/** Wraps the given middleware in a `MiddlewareChain` so it can be `.add()`ed onto */ export function chain< Needs extends EmptyContext, Adds extends EmptyContext = EmptyContext, >( middleware: Middleware, ): MiddlewareChain; -/** Wraps the given middleware in a `MiddlewareChain` so it can be `.add()`ed onto */ + export function chain< Needs extends EmptyContext, Adds extends EmptyContext = EmptyContext, From 0495ffba3e4f1c5009fa9f4fa7c343e29ee5d182 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 15:33:28 +0100 Subject: [PATCH 75/84] :white_check_mark: Add tests for middleware chain type safety --- http/middleware/discussion-pr.md | 64 +++++++++++-------- .../{example.ts => example_middleware.ts} | 24 ++----- http/middleware/example_server.ts | 9 +++ http/middleware_test.ts | 43 ++++++++++++- 4 files changed, 93 insertions(+), 47 deletions(-) rename http/middleware/{example.ts => example_middleware.ts} (66%) create mode 100644 http/middleware/example_server.ts diff --git a/http/middleware/discussion-pr.md b/http/middleware/discussion-pr.md index 4f7f47e16a45..9bff97397edc 100644 --- a/http/middleware/discussion-pr.md +++ b/http/middleware/discussion-pr.md @@ -1,24 +1,37 @@ -# `std/http/middleware` +# `std/http/` Request API and Middleware + +After delaying this again and again to polish it, here is a (evolved) review-ready version +of POC in the linked issue #1295 . + +Bedore I go into the ideas and changes here, I want to give some credt. Even though +I wrote the code, I want to explicitly mention and thank: + +- @keithamus for constant sparring and brainstorming during the process of writing this +- @happenslol for several feedback rounds on the API and inspiration from other languages +- @Hakizu , @Andreu and @grian for feedback on the docs ## Goals -- Make `std/http` API ergonomic and powerful enough to drive actual applications - out of the box +- Make the `std/http` API ergonomic and powerful enough to be ready to drive actual applications + reasonably out of the box at some point - Similar to most http frameworks, there should be an ergonomic central - `req => res` API offering all information needed in the context of a single + `req => res` API offering all information needed regarding a specific request - - Establish minimal but complete middleware pattern that can be used to build + - Establish a minimal but complete middleware pattern that can be used to build out-of-the-box and third-party middleware in the future to cover very common use cases like CORS, compresison, deserialization, logging etc. Middleware should be... - - ...just a function, there should be no magic, just a straight callstack + - ...just a function, there should be no magic and no additional IOC style runtime stuff, just a straight callstack - ...as composable and modularizable as possible + - ...able to handle Errors with normal JS constructs - ...type(script) safe to use and combine, ensuring correct middleware - context and order + context, order and termination. Middleware should not be less safe than + combining normal functions ## Changes -This PR contains the following changes: +I try to summarize the changes in the PR below, but I would highly encourage reading the docs - they +are an important part of the module and should be good at explaining the concepts. ### `HttpRequest` Delegate Wrapper @@ -29,8 +42,8 @@ unified API: - A `connInfo` property, holding the `connInfo` that was passed separately before -- A lazy `parsedUrl` property to access a `URL` object for the request url -- A `context` object to hold request context with an `.addContext()` method to +- A lazy `parsedUrl` property to access a `URL` object for the request url (as a start for convenience APIs) +- A `context` object to hold (initally empty) request context with an `.addContext()` method to add onto it Note that there should probably be a lot more convenience methods here - like a @@ -42,7 +55,7 @@ possible and clean up the API without bloating the PR too much. The `Handler` signature has been reduced to just `req: HttpRequest => Response`. -**This only breaks code that relies on the second `connInfo` argument that was +**This only breaks code that relies on the positional `connInfo` argument that was removed**. Code that just depends on the request will continue to work, as `HttpRequest implements Request`. @@ -53,38 +66,35 @@ established middleware signature in the change below. ### `Middleware` & `chain()` Adds a `chain()` utiltiy that allows to chain -`(req: HttpRequest, next: Handler) => Response` middleware together with very -little code in a type safe, order aware manner. Chains can be arbitrarily nested -and, given that their request context types are sound, are directly assignable -as `Handler`s: +`(req: HttpRequest, next: Handler) => Response` middleware together with very +little actual runtime code in a type safe, order aware manner. Chains can be arbitrarily nested, +understand when they are terminated and will generally stop you from running into runtime errors. + +Terminated chains are actual `Handler`s and thus cann be passed to `serve`: ```typescript const handler = chain(parseMiddleware) .add(validateMiddleware) .add(dataHandler); -await listenAndServe(":8000", handler); +await serve(handler); ``` The actual runtime code of `chain()` is very small (like 20 lines in total), but the type safety and resulting DX is made possible by typing code around the -`Middleware` type, which helps users to write middleware by infering the correct -types for parameters as well as forcing middleware authors to explicitly declare -their type impact on request context, allowing consumers to get LSP / compiler -feedback when using their middleware. +`Middleware` type, which is the TS way to write middleware, enforcing middleware +authors to be type safe. The typing code mostly constraints what one can do +to provide maximum safety and instant LSP feedback. ### README Docs -Adds brief docs on what middleware is in general, how to use it and how to write -it. - -Adds brief docs on `HttpRequest`. - -Also expands the other sections a bit, trying to streamline a storyline. +Adds a table of contents to the `README`, and a section about Middleware, +while integrating the other changes and lightly touching up some of the other +docs. Removes docs about the legacy server. ## Try it out -Under `http/middleware/example/ts` you find a server using the middleware +Under `http/middleware/example.ts` you find a server using the middleware examples from the docs. diff --git a/http/middleware/example.ts b/http/middleware/example_middleware.ts similarity index 66% rename from http/middleware/example.ts rename to http/middleware/example_middleware.ts index c46295b8e3a4..1045c94cf904 100644 --- a/http/middleware/example.ts +++ b/http/middleware/example_middleware.ts @@ -1,14 +1,7 @@ -import { - chain, - EmptyContext, - Handler, - Middleware, - serve, - Status, -} from "../mod.ts"; +import { EmptyContext, Handler, Middleware, Status } from "../mod.ts"; import { parse } from "../../encoding/yaml.ts"; -const log: Middleware = async (req, next) => { +export const log: Middleware = async (req, next) => { const start = performance.now(); const res = await next(req); const duration = performance.now() - start; @@ -20,7 +13,7 @@ const log: Middleware = async (req, next) => { return res; }; -const yaml: Middleware = async ( +export const yaml: Middleware = async ( req, next, ) => { @@ -31,7 +24,7 @@ const yaml: Middleware = async ( return await next(newReq); }; -const validate: Middleware<{ data: unknown }, { data: string[] }> = ( +export const validate: Middleware<{ data: unknown }, { data: string[] }> = ( req, next, ) => { @@ -47,7 +40,7 @@ const validate: Middleware<{ data: unknown }, { data: string[] }> = ( ); }; -const handle: Handler<{ data: string[] }> = (req) => { +export const handleGreetings: Handler<{ data: string[] }> = (req) => { const { data } = req.context; return new Response( @@ -56,10 +49,3 @@ const handle: Handler<{ data: string[] }> = (req) => { .join("\n"), ); }; - -const handler = chain(log) - .add(yaml) - .add(validate) - .add(handle); - -await serve(handler); diff --git a/http/middleware/example_server.ts b/http/middleware/example_server.ts new file mode 100644 index 000000000000..2245d82d88aa --- /dev/null +++ b/http/middleware/example_server.ts @@ -0,0 +1,9 @@ +import { chain, serve } from "../mod.ts"; +import { handleGreetings, log, validate, yaml } from "./example_middleware.ts"; + +const handler = chain(log) + .add(yaml) + .add(validate) + .add(handleGreetings); + +await serve(handler); diff --git a/http/middleware_test.ts b/http/middleware_test.ts index 7d67d3743faf..faa03b4bd9d2 100644 --- a/http/middleware_test.ts +++ b/http/middleware_test.ts @@ -1,4 +1,10 @@ -import { chain, Handler, HttpRequest, Middleware } from "./mod.ts"; +import { chain, Handler, HttpRequest, Middleware, serve } from "./mod.ts"; +import { + handleGreetings, + log, + validate, + yaml, +} from "./middleware/example_middleware.ts"; import { assertEquals } from "../testing/asserts.ts"; function buildRequest(body?: string) { @@ -104,3 +110,38 @@ Deno.test({ assertEquals(called, [1, 2, 3, 4]); }, }); + +Deno.test({ + name: `[http/middleware] unterminated chains should not be passable to serve`, + fn: () => { + const chained = chain(yaml).add(validate); + + const _ = () => { + // @ts-expect-error Should not be assignable + serve(chained); + }; + }, +}); + +Deno.test({ + name: + `[http/middleware] terminated chains that still depend on some context should not be passable to serve`, + fn: () => { + const chained = chain(validate).add(handleGreetings); + + const _ = () => { + // @ts-expect-error Should not be assignable + serve(chained); + }; + }, +}); + +Deno.test({ + name: `[http/middleware] terminated chains should not allow to call add`, + fn: () => { + const chained = chain(yaml).add(validate).add(handleGreetings); + + // @ts-expect-error Should not be assignable + chained.add(log); + }, +}); From c83df634fe55885a94722ca55661c979cb9c2291 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 15:48:31 +0100 Subject: [PATCH 76/84] :memo: Finish PR text --- http/middleware/discussion-pr.md | 62 +++++++++++++++++--------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/http/middleware/discussion-pr.md b/http/middleware/discussion-pr.md index 9bff97397edc..9f9dfb62b388 100644 --- a/http/middleware/discussion-pr.md +++ b/http/middleware/discussion-pr.md @@ -1,27 +1,31 @@ # `std/http/` Request API and Middleware -After delaying this again and again to polish it, here is a (evolved) review-ready version -of POC in the linked issue #1295 . +After delaying this again and again to polish it, here is a (evolved) +review-ready version of POC in the linked issue #1295 . -Bedore I go into the ideas and changes here, I want to give some credt. Even though -I wrote the code, I want to explicitly mention and thank: +Bedore I go into the ideas and changes here, I want to give some credt. Even +though I wrote the code, I want to explicitly mention and thank: -- @keithamus for constant sparring and brainstorming during the process of writing this -- @happenslol for several feedback rounds on the API and inspiration from other languages +- @keithamus for constant sparring and brainstorming during the process of + writing this +- @happenslol for several feedback rounds on the API and inspiration from other + languages - @Hakizu , @Andreu and @grian for feedback on the docs ## Goals -- Make the `std/http` API ergonomic and powerful enough to be ready to drive actual applications - reasonably out of the box at some point +- Make the `std/http` API ergonomic and powerful enough to be ready to drive + actual applications reasonably out of the box at some point as well as offer + smooth out of the box solutions for `deno deploy` - Similar to most http frameworks, there should be an ergonomic central `req => res` API offering all information needed regarding a specific request - - Establish a minimal but complete middleware pattern that can be used to build - out-of-the-box and third-party middleware in the future to cover very common - use cases like CORS, compresison, deserialization, logging etc. Middleware - should be... - - ...just a function, there should be no magic and no additional IOC style runtime stuff, just a straight callstack + - Establish a minimal but complete middleware pattern that can be used to + build out-of-the-box and third-party middleware in the future to cover very + common use cases like CORS, compresison, deserialization, logging etc. + Middleware should be... + - ...just a function, there should be no magic and no additional IOC style + runtime stuff, just a straight callstack - ...as composable and modularizable as possible - ...able to handle Errors with normal JS constructs - ...type(script) safe to use and combine, ensuring correct middleware @@ -30,8 +34,9 @@ I wrote the code, I want to explicitly mention and thank: ## Changes -I try to summarize the changes in the PR below, but I would highly encourage reading the docs - they -are an important part of the module and should be good at explaining the concepts. +I try to summarize the changes in the PR below, but I would highly encourage +reading the docs - they are an important part of the module and should be good +at explaining the concepts. ### `HttpRequest` Delegate Wrapper @@ -42,9 +47,10 @@ unified API: - A `connInfo` property, holding the `connInfo` that was passed separately before -- A lazy `parsedUrl` property to access a `URL` object for the request url (as a start for convenience APIs) -- A `context` object to hold (initally empty) request context with an `.addContext()` method to - add onto it +- A lazy `parsedUrl` property to access a `URL` object for the request url (as a + start for convenience APIs) +- A `context` object to hold (initally empty) request context with an + `.addContext()` method to add onto it Note that there should probably be a lot more convenience methods here - like a (`CookieStore` compatible?) lazy cookie reading API, maybe a way to access @@ -55,8 +61,8 @@ possible and clean up the API without bloating the PR too much. The `Handler` signature has been reduced to just `req: HttpRequest => Response`. -**This only breaks code that relies on the positional `connInfo` argument that was -removed**. Code that just depends on the request will continue to work, as +**This only breaks code that relies on the positional `connInfo` argument that +was removed**. Code that just depends on the request will continue to work, as `HttpRequest implements Request`. This cleans up the API, creating a place to add more request information onto in @@ -66,9 +72,10 @@ established middleware signature in the change below. ### `Middleware` & `chain()` Adds a `chain()` utiltiy that allows to chain -`(req: HttpRequest, next: Handler) => Response` middleware together with very -little actual runtime code in a type safe, order aware manner. Chains can be arbitrarily nested, -understand when they are terminated and will generally stop you from running into runtime errors. +`(req: HttpRequest, next: Handler) => Response` middleware together with very +little actual runtime code in a type safe, order aware manner. Chains can be +arbitrarily nested, understand when they are terminated and will generally stop +you from running into runtime errors. Terminated chains are actual `Handler`s and thus cann be passed to `serve`: @@ -83,14 +90,13 @@ await serve(handler); The actual runtime code of `chain()` is very small (like 20 lines in total), but the type safety and resulting DX is made possible by typing code around the `Middleware` type, which is the TS way to write middleware, enforcing middleware -authors to be type safe. The typing code mostly constraints what one can do -to provide maximum safety and instant LSP feedback. +authors to be type safe. The typing code mostly constraints what one can do to +provide maximum safety and instant LSP feedback. ### README Docs -Adds a table of contents to the `README`, and a section about Middleware, -while integrating the other changes and lightly touching up some of the other -docs. +Adds a table of contents to the `README`, and a section about Middleware, while +integrating the other changes and lightly touching up some of the other docs. Removes docs about the legacy server. From 99d101f27e672c1e6efdcec6b148a2261a86f519 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 15:49:00 +0100 Subject: [PATCH 77/84] :fire: Remove text draft files --- http/middleware/discussion-pr.md | 106 ---------------- http/middleware/discussion.md | 208 ------------------------------- 2 files changed, 314 deletions(-) delete mode 100644 http/middleware/discussion-pr.md delete mode 100644 http/middleware/discussion.md diff --git a/http/middleware/discussion-pr.md b/http/middleware/discussion-pr.md deleted file mode 100644 index 9f9dfb62b388..000000000000 --- a/http/middleware/discussion-pr.md +++ /dev/null @@ -1,106 +0,0 @@ -# `std/http/` Request API and Middleware - -After delaying this again and again to polish it, here is a (evolved) -review-ready version of POC in the linked issue #1295 . - -Bedore I go into the ideas and changes here, I want to give some credt. Even -though I wrote the code, I want to explicitly mention and thank: - -- @keithamus for constant sparring and brainstorming during the process of - writing this -- @happenslol for several feedback rounds on the API and inspiration from other - languages -- @Hakizu , @Andreu and @grian for feedback on the docs - -## Goals - -- Make the `std/http` API ergonomic and powerful enough to be ready to drive - actual applications reasonably out of the box at some point as well as offer - smooth out of the box solutions for `deno deploy` - - Similar to most http frameworks, there should be an ergonomic central - `req => res` API offering all information needed regarding a specific - request - - Establish a minimal but complete middleware pattern that can be used to - build out-of-the-box and third-party middleware in the future to cover very - common use cases like CORS, compresison, deserialization, logging etc. - Middleware should be... - - ...just a function, there should be no magic and no additional IOC style - runtime stuff, just a straight callstack - - ...as composable and modularizable as possible - - ...able to handle Errors with normal JS constructs - - ...type(script) safe to use and combine, ensuring correct middleware - context, order and termination. Middleware should not be less safe than - combining normal functions - -## Changes - -I try to summarize the changes in the PR below, but I would highly encourage -reading the docs - they are an important part of the module and should be good -at explaining the concepts. - -### `HttpRequest` Delegate Wrapper - -Adds an `HttpRequest`-class (got a better name? please say so!) that wraps a -`Request` object, delegating to it (and thus implementing the `Request` API -interface itself) and adding convenience methods and properties to have a -unified API: - -- A `connInfo` property, holding the `connInfo` that was passed separately - before -- A lazy `parsedUrl` property to access a `URL` object for the request url (as a - start for convenience APIs) -- A `context` object to hold (initally empty) request context with an - `.addContext()` method to add onto it - -Note that there should probably be a lot more convenience methods here - like a -(`CookieStore` compatible?) lazy cookie reading API, maybe a way to access -parsed content types lazily etc - I just wanted to create a space where that is -possible and clean up the API without bloating the PR too much. - -### **BREAKING**: `Handler` Signature - -The `Handler` signature has been reduced to just `req: HttpRequest => Response`. - -**This only breaks code that relies on the positional `connInfo` argument that -was removed**. Code that just depends on the request will continue to work, as -`HttpRequest implements Request`. - -This cleans up the API, creating a place to add more request information onto in -`HttpRequest` instead of adding more parameters while also enabling the -established middleware signature in the change below. - -### `Middleware` & `chain()` - -Adds a `chain()` utiltiy that allows to chain -`(req: HttpRequest, next: Handler) => Response` middleware together with very -little actual runtime code in a type safe, order aware manner. Chains can be -arbitrarily nested, understand when they are terminated and will generally stop -you from running into runtime errors. - -Terminated chains are actual `Handler`s and thus cann be passed to `serve`: - -```typescript -const handler = chain(parseMiddleware) - .add(validateMiddleware) - .add(dataHandler); - -await serve(handler); -``` - -The actual runtime code of `chain()` is very small (like 20 lines in total), but -the type safety and resulting DX is made possible by typing code around the -`Middleware` type, which is the TS way to write middleware, enforcing middleware -authors to be type safe. The typing code mostly constraints what one can do to -provide maximum safety and instant LSP feedback. - -### README Docs - -Adds a table of contents to the `README`, and a section about Middleware, while -integrating the other changes and lightly touching up some of the other docs. - -Removes docs about the legacy server. - -## Try it out - -Under `http/middleware/example.ts` you find a server using the middleware -examples from the docs. diff --git a/http/middleware/discussion.md b/http/middleware/discussion.md deleted file mode 100644 index 6b4d18daf78a..000000000000 --- a/http/middleware/discussion.md +++ /dev/null @@ -1,208 +0,0 @@ -# `std/http/middleware` Concept - -## Goals - -- **Establish a middleware concept that enables `std/http` to be used for actual - applications** directly in the future. Once a pattern is established, there - are already some modules in `std` that could easily be wrapped into - out-of-the-box middleware -- **Allow middleware and composed middleware stacks to just be a function** that - takes some form of request and returns a response, optionally calling the next - middleware. This ensures that we deal with normal call stacks, allows errors - to bubble up as expected and reduces the amount of black magic happening at - runtime -- **Be completely type safe, including middleware order and arbitrary middleware - composition.** This means I want the type checker to stop me from registering - a handler on the server that assumes that certain information is available - from previous middlewares (e.g. auth info, parsed and validated bodies...), - even though that middleware is not present in that handler's chain. Just - having a global state that can be typed and is assumed to always be present in - every function is not good enough - we are dealing with a chain of functions - here, we should leverage Typescript and make sure that that chain actually - works type-wise. -- The middleware signature should **be compatible to the `Server`s `Handler` - signature**. Composing middleware should always just return a new middleware, - so that compositions can be modularized and passed around opaquely - -## POC - -[Here is a branch](https://github.com/LionC/deno_std/tree/middleware-experiment/http) -in which I have built a small dirty POC fullfiling the goals above. **This is -just to show the idea**. It is not fleshed out, very rough around a lot of -edges, has subpar ergonomics and several straight up bugs. All of them are -solvable in several ways and their solution is not vital to the concept, so I -left them as they are for the sake of starting a conversation. - -I stopped writing as soon as I was sure enough that this can be done reasonably. -There are many ways to do this basic concept and a lot of them are viable - I -did not want to invest into one of them, just have something to start talking. - -### API - -The POC contains three components. Their actual runtime code is really small - -most of the code around it (and most todos to fix the bugs / ergonomics issues) -is just types. - -The components are: - -- A `Middleware` function type with two important generic type parameters: - - What type the middleware expects (e.g. it needs a semantic auth field on top - of the normal request) - - Optionally, what information the middleware adds to the request "context" - (e.g. validating the body to be a valid `Animal` and adding an - `animal: Animal` property) It could be used like this (lots of abstracted - functions in here to show the idea): - - ```typescript - const validateFoo: Middleware = async ( - req, - con, - next, - ) => { - const body = extractBody(req); - - if (!isFoo(body)) { - return new Response( - "Invalid Foo", - { status: 422 }, - ); - } - - const nextReq = extend(req, { foo: body }); - - return await next!(nextReq, con); - }; - ``` -- A `composeMiddleware` function that takes two `Middleware`s and returns a new - `Middleware` that is a composition of both in the given order. The resulting - `Middleware` adds a union of what both arguments add and requires a union of - what both arguments require, except the intersection between what the first - one adds and the second one requires, as that has already been satisfied - within the composition. - - It could be used like that: - - ```typescript - declare const authenticate: Middleware; - declare const authorize: Middleware; - - const checkAccess = composeMiddleware(authenticate, authorize); - - assertType>(checkAccess); - ``` - - `composeMiddleware` is the atomic composition and type checking step but not - very ergonomic to use, as it can only handle two middlewares being combined. -- A `stack` helper that wraps a given `Middleware` in an object thas has a - chainable `.add()` method. This allows for nicer usage and follows the usual - `.use()` idea in spirit. It can be used like this: - - ```typescript - declare const authenticate: Middleware; - declare const authorize: Middleware; - declare const validateFoo: Middleware; - - const authAndValidate = stack(authenticate) - .add(authorize) - .add(validateFoo) - .handler; - - assertType>( - authAndValidate, - ); - ``` - - This essentially just wraps `composeMiddleware` to be chainable with correct - typing. - - Notice the `.handler` at the end - this extracts the actual function again. - There might be nicer ways to do it, but the concept works for the sake of - discussion. - -The components above fulfill the goals mentioned above: - -- `Middleware` is just a function, including the result of an arbitrary - `stack().add().add().add().handler` chain -- `Middleware` is assignable to `std/http` `Handler` - meaning there is - no additional wrapping necessary -- Middleware composition is completely type safe and order-aware. This means - that all requirements that are present but not fulfilled by previous - middleware "bubble up" and will type error when trying to register it on the - `Server`, stating which properties are missing - -To be fair, it makes some assumptions. It assumes that you always add the same -type to your `next` call, so if you have conditional `next` calls with different -types, you need to "flatten" the types. It also assumes that you do not throw -away the previous request context. However, I think those are reasonable -assumptions and they are also present (and a lot less safe) in other current TS -middleware concepts e.g. in koa / oak. - -### Play around with it - -To run a small server with some middleware from the POC branch, follow the steps -below. **The implemented middleware is just for presentation purposes**, it's -implementation is very bad, but it works to show the idea. - -1. Check out the branch, e.g. with - - ```sh - git remote add lionc git@github.com:LionC/deno_std.git - git fetch lionc - git switch middleware-experiment - ``` -2. Start the server with - - ```sh - deno run --allow-net http/middleware/poc/server.ts - ``` - -Now you can throw some requests at it, here are some `httpie` example commands: - -- Succeed - - ```sh - http --json 0.0.0.0:5000/ name=My entryFee:=10 animals:='[{"name": "Kim", "kind": "Tiger"}, {"name": "Flippo", "kind": "Hippo"}, {"name": "Jasmin", "kind": "Tiger"}]' - ``` -- Fail validation: - - ```sh - $ http --json 0.0.0.0:5000/ name=My entryFee:=10 - ``` -- Fail JSON content type: - - ```sh - $ http --form 0.0.0.0:5000/ name=My entryFee:=10 - ``` - -`http/middleware/poc/server.ts` is also a good place to play around with the -type safe composition - try changing the order of middleware, leave a vital one -out and see how LSP / tsc react. - -## What now? - -There are two questions to answer here: - -- What have I missed? Is this something we want to go deeper on? I did not want - to invest more time into figuring out all the details before there is some - input on the core idea -- How do we want the API for application and middleware authors to look like? - See my take on `Request` below. The pattern above works either way, but I - think we should take a look at that. - -### On `Request` and API ergonomic - -While working on this and trying to write some middlewares, I really felt that -the current `Handler` signature is quite...weird. I get why it looks that way, -but from an API perspective, it does not make a lot of sense that two arbitrary -fields about the incoming request are separated into their own argument. It also -does not make a lot of sense that some arbitrary functionality that would be -expected on the request parameter needs to be separately `import`ed as a -function and called on that object. There is also not really a nice way to add -new things to a request in a type safe way. - -Following `Request` makes a lot of sense, it being a Web standard and all. But I -think it could make sense to `extend` `Request` in `std/http` to have one -central API for everything concerning the incoming request - including -`connInfo`, a simple helper to add to some kind of request context, helpers to -get common info like parsed content types, get cookies etc while still following -`Request` for everything it offers. From 9a9a18d4a6a8e779c39822648ab93f94a4c7eced Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 15:59:04 +0100 Subject: [PATCH 78/84] :rotating_light: Fix docs misisng imports --- http/README.md | 2 +- http/middleware.ts | 19 ++++++++++++------- http/request.ts | 2 ++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/http/README.md b/http/README.md index 162d9970f5e4..512851901fa3 100644 --- a/http/README.md +++ b/http/README.md @@ -79,7 +79,7 @@ import { const handle: Handler = (req) => { if (req.method !== Method.Get) { // Will respond with an empty 404 - return new Reponse(null, { status: Status.NotFound }); + return new Response(null, { status: Status.NotFound }); } return new Response("Hello!"); diff --git a/http/middleware.ts b/http/middleware.ts index b04bd8a533eb..53dbda0c6286 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -17,10 +17,11 @@ import type { CommonKeys, Expand, SafeOmit } from "../_util/types.ts"; * * Example: * -* ```ts import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; +* ```ts +* import { Middleware } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; * * // Log response time, method and path for requests going through this middleware -* const log: Middleware = (req, next) => { +* const log: Middleware = async (req, next) => { * const start = performance.now(); * const response = await next(req); * const end = performance.now(); @@ -60,17 +61,19 @@ export type MiddlewareChain< * Example: * * ```ts - * const first: Middleware = (req, next) => { + * import { Middleware, Handler, chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; + * + * const first: Middleware = async (req, next) => { * console.log("Hey"); * return await next(req); * }; * - * const second: Middleware = (req, next) => { + * const second: Middleware = async (req, next) => { * console.log("there!"); * return await next(req); * }; * - * const helloWorld = (req) => new Response("Hello world"); + * const helloWorld: Handler = (req) => new Response("Hello world"); * * // This Handler will log "Hey", log "there!" and then respond with "Hello World" * const handler = chain(first) @@ -91,12 +94,14 @@ export type MiddlewareChain< * Example: * * ```ts - * const first: Middleware = (req, next) => { + * import { Middleware, chain } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; + * + * const first: Middleware = async (req, next) => { * console.log("Hey"); * return await next(req); * }; * - * const second: Middleware = (req, next) => { + * const second: Middleware = async (req, next) => { * console.log("there!"); * return await next(req); * }; diff --git a/http/request.ts b/http/request.ts index 0f97cea16065..fbf1e72cea57 100644 --- a/http/request.ts +++ b/http/request.ts @@ -43,6 +43,8 @@ export class HttpRequest * Example: * * ```ts + * import { HttpRequest } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; + * * declare const req: HttpRequest * * const reqWithUser = req.addContext({ user: "Example" }) From 306a79084bdb8eca55a5319000c37aba2b7e4dd2 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 16:00:38 +0100 Subject: [PATCH 79/84] :rotating_light: Add missing assert import in JSDoc --- http/request.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/http/request.ts b/http/request.ts index fbf1e72cea57..dedc0aa09471 100644 --- a/http/request.ts +++ b/http/request.ts @@ -44,6 +44,7 @@ export class HttpRequest * * ```ts * import { HttpRequest } from "https://deno.land/std@$STD_VERSION/http/mod.ts"; + * import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; * * declare const req: HttpRequest * From b2353c60f805d21902bfc798f373f9edc1802214 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 16:03:45 +0100 Subject: [PATCH 80/84] :fire: Remove empty legacy server docs --- http/legacy_server.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 http/legacy_server.md diff --git a/http/legacy_server.md b/http/legacy_server.md deleted file mode 100644 index e69de29bb2d1..000000000000 From 8ccd7a3d23f73fd626136b8cf36f34eb34647458 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 16:05:25 +0100 Subject: [PATCH 81/84] :page_facing_up: Add copyright headers --- http/middleware.ts | 2 ++ http/middleware/example_middleware.ts | 2 ++ http/middleware/example_server.ts | 2 ++ http/middleware_test.ts | 2 ++ http/request.ts | 2 ++ 5 files changed, 10 insertions(+) diff --git a/http/middleware.ts b/http/middleware.ts index 53dbda0c6286..f9bc512e7282 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -1,3 +1,5 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + import type { Handler } from "./server.ts"; import type { EmptyContext, HttpRequest } from "./request.ts"; import type { CommonKeys, Expand, SafeOmit } from "../_util/types.ts"; diff --git a/http/middleware/example_middleware.ts b/http/middleware/example_middleware.ts index 1045c94cf904..feb8429c4ba0 100644 --- a/http/middleware/example_middleware.ts +++ b/http/middleware/example_middleware.ts @@ -1,3 +1,5 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + import { EmptyContext, Handler, Middleware, Status } from "../mod.ts"; import { parse } from "../../encoding/yaml.ts"; diff --git a/http/middleware/example_server.ts b/http/middleware/example_server.ts index 2245d82d88aa..d5b92cbf4231 100644 --- a/http/middleware/example_server.ts +++ b/http/middleware/example_server.ts @@ -1,3 +1,5 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + import { chain, serve } from "../mod.ts"; import { handleGreetings, log, validate, yaml } from "./example_middleware.ts"; diff --git a/http/middleware_test.ts b/http/middleware_test.ts index faa03b4bd9d2..5040630bd98b 100644 --- a/http/middleware_test.ts +++ b/http/middleware_test.ts @@ -1,3 +1,5 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + import { chain, Handler, HttpRequest, Middleware, serve } from "./mod.ts"; import { handleGreetings, diff --git a/http/request.ts b/http/request.ts index dedc0aa09471..1511fc4eae9e 100644 --- a/http/request.ts +++ b/http/request.ts @@ -1,3 +1,5 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + import { ConnInfo } from "./server.ts"; import { Expand } from "../_util/types.ts"; From f3b816b9e9195dca9d4f23443747e5750fa36a23 Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 11 Nov 2021 16:32:30 +0100 Subject: [PATCH 82/84] :white_check_mark: Update server tests for new Handler --- http/server_test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/http/server_test.ts b/http/server_test.ts index 95d01e0c0479..4e39a2bf4f0f 100644 --- a/http/server_test.ts +++ b/http/server_test.ts @@ -6,7 +6,8 @@ import { serveListener, Server, serveTls, -} from "./server.ts"; + HttpRequest, +} from "./mod.ts"; import { mockConn as createMockConn } from "./_mock_conn.ts"; import { dirname, fromFileUrl, join, resolve } from "../path/mod.ts"; import { readAll, writeAll } from "../streams/conversion.ts"; @@ -1040,11 +1041,11 @@ Deno.test("Server.serve can be called multiple times", async () => { const listenerOne = Deno.listen(listenerOneOptions); const listenerTwo = Deno.listen(listenerTwoOptions); - const handler = (_request: Request, connInfo: ConnInfo) => { - if ((connInfo.localAddr as Deno.NetAddr).port === listenerOneOptions.port) { + const handler = (request: HttpRequest) => { + if ((request.connInfo.localAddr as Deno.NetAddr).port === listenerOneOptions.port) { return new Response("Hello listener one!"); } else if ( - (connInfo.localAddr as Deno.NetAddr).port === listenerTwoOptions.port + (request.connInfo.localAddr as Deno.NetAddr).port === listenerTwoOptions.port ) { return new Response("Hello listener two!"); } @@ -1115,9 +1116,9 @@ Deno.test("Handler is called with the request instance and connection informatio let receivedRequest: Request; let receivedConnInfo: ConnInfo; - const handler = (request: Request, connInfo: ConnInfo) => { + const handler = (request: HttpRequest) => { receivedRequest = request; - receivedConnInfo = connInfo; + receivedConnInfo = request.connInfo; return new Response("Hello Deno!"); }; From 509c7f45396be4a9deccbf7b0040a61dcf52bcfa Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Fri, 12 Nov 2021 17:25:29 +0100 Subject: [PATCH 83/84] :fire: Remove old todo --- http/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/middleware.ts b/http/middleware.ts index f9bc512e7282..a5ee448fd2e1 100644 --- a/http/middleware.ts +++ b/http/middleware.ts @@ -116,7 +116,7 @@ export type MiddlewareChain< add( middleware: Middleware, ): MiddlewareChain< - Expand>, // todo does safeomit handle assignability? e.g. require string, provide 'a' + Expand>, Expand> >; }; From 1b0641c09a681447c5c8094ffeafeb7fa6eb504a Mon Sep 17 00:00:00 2001 From: Leon Strauss Date: Thu, 9 Dec 2021 15:36:24 +0100 Subject: [PATCH 84/84] :art: Format --- http/server_test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/http/server_test.ts b/http/server_test.ts index 2d823b3a38c0..0f96c7d18226 100644 --- a/http/server_test.ts +++ b/http/server_test.ts @@ -2,11 +2,11 @@ import { _parseAddrFromStr, ConnInfo, + HttpRequest, serve, serveListener, Server, serveTls, - HttpRequest, } from "./mod.ts"; import { mockConn as createMockConn } from "./_mock_conn.ts"; import { dirname, fromFileUrl, join, resolve } from "../path/mod.ts"; @@ -1042,10 +1042,14 @@ Deno.test("Server.serve can be called multiple times", async () => { const listenerTwo = Deno.listen(listenerTwoOptions); const handler = (request: HttpRequest) => { - if ((request.connInfo.localAddr as Deno.NetAddr).port === listenerOneOptions.port) { + if ( + (request.connInfo.localAddr as Deno.NetAddr).port === + listenerOneOptions.port + ) { return new Response("Hello listener one!"); } else if ( - (request.connInfo.localAddr as Deno.NetAddr).port === listenerTwoOptions.port + (request.connInfo.localAddr as Deno.NetAddr).port === + listenerTwoOptions.port ) { return new Response("Hello listener two!"); }