make-fetch 2.3.4 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -7,8 +7,10 @@ Implement your own `fetch()` with node.js streams
7
7
  npm i --save make-fetch
8
8
  ```
9
9
 
10
+ Basic example:
11
+
10
12
  ```javascript
11
- const makeFetch = require('make-fetch')
13
+ import { makeFetch } from 'make-fetch'
12
14
  const fetch = makeFetch(async (request) => {
13
15
  const {
14
16
  url, // String representing request URL
@@ -20,11 +22,11 @@ const fetch = makeFetch(async (request) => {
20
22
  } = request
21
23
 
22
24
  return {
23
- statusCode: 200, // Should specify the status code to send back
25
+ status: 200, // Should specify the status code to send back
24
26
  headers: { // Optional object mapping response header titles to values
25
27
  "something": "whatever"
26
28
  },
27
- data: asyncIterator // Required async iterable for the response body, can be empty
29
+ body: asyncIterator // Required async iterable for the response body, can be empty
28
30
  }
29
31
  })
30
32
 
@@ -32,7 +34,66 @@ const response = await fetch('myscheme://whatever/foobar')
32
34
  console.log(await response.text())
33
35
  ```
34
36
 
35
- ## Gotchas
37
+ Routed example:
38
+
39
+ ```JavaScript
40
+
41
+ import {makeRoutedFetch} from "make-fetch"
42
+
43
+ const {fetch, router} = makeRoutedFetch()
44
+
45
+ router.get('example://somehost/**', (request) => {
46
+ return {
47
+ body: "hello world",
48
+ headers: {example: "Whatever"}
49
+ }
50
+ })
51
+
52
+ // You can have wildcards in the protocol, hostname,
53
+ // or individual segments in the pathname
54
+ router.post('*://*/foo/*/bar/, () => {
55
+ return {body: 'Goodbye world'}
56
+ })
57
+
58
+ // Match first handler
59
+ fetch('example://somehost/example/path/here')
60
+
61
+ // Match second handler
62
+ fetch('whatever://something/foo/whatever/bar/')
63
+
64
+ ```
65
+
66
+ ### API:
67
+
68
+ `makeFetch(async (Request) => ResponseOptions) => fetch()`
69
+
70
+ The main API is based on the handler which takes a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object, and must return options for constructing a response based on the [Response](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) constructor.
71
+
72
+ Instead of having a separate parameter for the body and the response options, the fetch handler should return both in one object.
73
+
74
+ This will then return a standard [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) API which takes request info, and returns responses.
75
+
76
+ `makeRoutedFetch({onNotFound, onError}) => {router: Router, fetch}`
77
+
78
+ If you want to have an easier way of routing different methods/hostnames/paths, you can use a routed make-fetch which can make it easier to register handlers for different routes.
79
+ This will creat a Router, and a `fetch()` instance for that router.
80
+ Handlers you add on the router will be useful to match URLs+methods from the fetch request and will use the matched handler to generate the response.
81
+ You can optionally supply a `onNotFound` handler to be invoked if no other routes match.
82
+ You can optionally supply a `onError` handler to be invoked when an error is thrown from your handlers which will take the `Error` instance and the `Request` instance as arguments.
83
+ The default `onError` handler will print the stack trace to the body with a `500` status code.
84
+
85
+ `router.add(method, urlPattern, handler) => Router`
86
+
87
+ You can add routes for specific methods, and use URL patterns.
88
+ Then you can pass in the same basic handler as in makeFetch.
89
+ You can chain multiple add requests since the router returns itself when adding a route.
90
+
91
+ `router.get/head/put/post/delete(urlPattern, handler) => Router`
92
+
93
+ You can also use shorthands for methods with a similar API.
94
+
95
+ `router.any(urlPattern, handler)`
96
+
97
+ You can register handlers for any method.
36
98
 
37
- - The `response.body` is an Async Iterable of Buffer objects rather than a WHATWG ReadableStream
38
- - Eventually ReadableStream will become async iterable so you'll be able to iterate either normally
99
+ For example `router.any('*://*/**', handler)` will register a handler that will be invoked on any method/protocol scheme/hostname/path.
package/index.js CHANGED
@@ -1,122 +1,162 @@
1
- const Headers = require('fetch-headers')
2
- const getStatus = require('statuses')
3
- const bodyToIterator = require('fetch-request-body-to-async-iterator')
4
- const { TransformStream } = require('web-streams-polyfill/ponyfill/es6')
5
-
6
- module.exports = function makeFetch (handler) {
7
- return async function fetch (resource, init = {}) {
8
- if (!resource) throw new Error('Must specify resource')
9
- if (typeof resource === 'string') {
10
- return fetch({
11
- ...(init || {}),
12
- url: resource
13
- })
14
- } else if (resource instanceof URL) {
15
- return fetch(resource.href, init)
1
+ export const WILDCARD = '*'
2
+
3
+ const MATCH_ORDER = ['method', 'protocol', 'hostname', 'pathname']
4
+
5
+ export function makeFetch (handler, {
6
+ Request = globalThis.Request,
7
+ Response = globalThis.Response
8
+ } = {}) {
9
+ return async function fetch (...requestOptions) {
10
+ const request = new Request(...requestOptions)
11
+
12
+ const { body = null, ...responseOptions } = await handler(request)
13
+
14
+ const response = new Response(body, responseOptions)
15
+
16
+ return response
17
+ }
18
+ }
19
+
20
+ export function makeRoutedFetch ({
21
+ onNotFound = DEFAULT_NOT_FOUND,
22
+ onError = DEFAULT_ON_ERROR
23
+ } = {}) {
24
+ const router = new Router()
25
+
26
+ const fetch = makeFetch(async (request) => {
27
+ const route = router.route(request)
28
+ if (!route) {
29
+ return onNotFound(request)
30
+ }
31
+ try {
32
+ return route.handler(request)
33
+ } catch (e) {
34
+ return await onError(e, request)
16
35
  }
36
+ })
37
+
38
+ return { fetch, router }
39
+ }
40
+
41
+ export function DEFAULT_NOT_FOUND () {
42
+ return { status: 404, statusText: 'Invalid URL' }
43
+ }
17
44
 
18
- const {
19
- session,
20
- url,
21
- headers: rawHeaders,
22
- method: rawMethod,
23
- body: rawBody,
24
- referrer,
25
- signal
26
- } = resource
27
-
28
- const headers = rawHeaders ? headersToObject(rawHeaders) : {}
29
- const method = (rawMethod || 'GET').toUpperCase()
30
- const body = rawBody ? bodyToIterator(rawBody, session) : null
31
-
32
- const {
33
- statusCode,
34
- statusText: rawStatusText,
35
- headers: rawResponseHeaders,
36
- data
37
- } = await handler({
38
- url,
39
- headers,
40
- method,
41
- body,
42
- referrer,
43
- signal
44
- })
45
-
46
- const responseHeaders = new Headers(rawResponseHeaders || {})
47
- const statusText = rawStatusText || getStatus(statusCode)
48
-
49
- return new FakeResponse(statusCode, statusText, responseHeaders, data, url)
45
+ export function DEFAULT_ON_ERROR (e) {
46
+ return {
47
+ status: 500,
48
+ headers: {
49
+ 'Content-Type': 'text/plain; charset=utf-8'
50
+ },
51
+ body: e.stack
50
52
  }
51
53
  }
52
54
 
53
- class FakeResponse {
54
- constructor (status, statusText, headers, iterator, url) {
55
- this.body = makeBody(iterator)
56
- this.headers = headers
57
- this.url = url
58
- this.status = status
59
- this.statusText = statusText
55
+ export class Router {
56
+ constructor () {
57
+ this.routes = []
60
58
  }
61
59
 
62
- get ok () {
63
- return this.status && this.status < 400
60
+ get (url, handler) {
61
+ return this.add('GET', url, handler)
64
62
  }
65
63
 
66
- get useFinalURL () {
67
- return true
64
+ head (url, handler) {
65
+ return this.add('HEAD', url, handler)
68
66
  }
69
67
 
70
- async arrayBuffer () {
71
- const buffer = await collectBuffers(this.body)
72
- const { byteOffset, length } = buffer
73
- const end = byteOffset + length
74
- return buffer.buffer.slice(buffer.byteOffset, end)
68
+ post (url, handler) {
69
+ return this.add('POST', url, handler)
75
70
  }
76
71
 
77
- async text () {
78
- const buffer = await collectBuffers(this.body)
79
- return buffer.toString('utf-8')
72
+ put (url, handler) {
73
+ return this.add('PUT', url, handler)
80
74
  }
81
75
 
82
- async json () {
83
- return JSON.parse(await this.text())
76
+ delete (url, handler) {
77
+ return this.add('DELETE', url, handler)
84
78
  }
85
- }
86
79
 
87
- function makeBody (iterator) {
88
- const { readable, writable } = new TransformStream();
89
- (async () => {
90
- try {
91
- const writer = writable.getWriter()
92
- try {
93
- for await (const x of iterator) await writer.write(x)
94
- } finally {
95
- await writer.close()
96
- }
97
- } catch {
98
- // There was some sort of unhandled error?
80
+ patch (url, handler) {
81
+ return this.add('PATCH', url, handler)
82
+ }
83
+
84
+ any (url, handler) {
85
+ return this.add(WILDCARD, url, handler)
86
+ }
87
+
88
+ add (method, url, handler) {
89
+ const parsed = new URL(url)
90
+ const { hostname, protocol, pathname } = parsed
91
+ const segments = pathname.slice(1).split('/')
92
+
93
+ const route = {
94
+ protocol,
95
+ method: method.toUpperCase(),
96
+ hostname,
97
+ segments,
98
+ handler
99
99
  }
100
- })()
101
- return readable
102
- }
103
100
 
104
- function headersToObject (headers) {
105
- if (!headers) return {}
106
- if (typeof headers.entries === 'function') {
107
- const result = {}
108
- for (const [key, value] of headers) {
109
- result[key] = value
101
+ this.routes.push(route)
102
+ return this
103
+ }
104
+
105
+ route (request) {
106
+ for (const route of this.routes) {
107
+ let hasFail = false
108
+ for (const property of MATCH_ORDER) {
109
+ if (!matches(request, route, property)) {
110
+ hasFail = true
111
+ }
112
+ }
113
+ if (!hasFail) {
114
+ return route
115
+ }
110
116
  }
111
- return result
112
- } else return headersToObject(new Headers(headers || {}))
117
+ return null
118
+ }
113
119
  }
114
120
 
115
- async function collectBuffers (iterable) {
116
- const all = []
117
- for await (const buff of iterable) {
118
- all.push(Buffer.from(buff))
121
+ function matches (request, route, property) {
122
+ if (property === 'pathname') {
123
+ const routeSegments = route.segments
124
+ const { pathname } = new URL(request.url)
125
+ const requestSegments = pathname.slice(1).split('/')
126
+
127
+ let i = 0
128
+ while (true) {
129
+ const routeLast = i === (routeSegments.length - 1)
130
+ const requestLast = i === (requestSegments.length - 1)
131
+
132
+ const routeSegment = routeSegments[i]
133
+ const requestSegment = requestSegments[i]
134
+ const routeWild = routeSegment === WILDCARD
135
+ const matches = routeWild || (routeSegment === requestSegment)
136
+
137
+ if (routeLast) {
138
+ if (routeSegment === (WILDCARD + WILDCARD)) return true
139
+ if (requestLast) return matches
140
+ return false
141
+ } else if (requestLast) {
142
+ return false
143
+ }
144
+
145
+ if (!matches) return false
146
+ i++
147
+ }
148
+ } if (property === 'hostname') {
149
+ return areEqual(route.hostname, new URL(request.url).hostname)
150
+ } else if (property === 'protocol') {
151
+ return areEqual(route.protocol, new URL(request.url).protocol)
152
+ } else {
153
+ const routeProperty = route[property]
154
+ const requestProperty = request[property]
155
+ return areEqual(routeProperty, requestProperty)
119
156
  }
157
+ }
120
158
 
121
- return Buffer.concat(all)
159
+ function areEqual (routeProperty, requestProperty) {
160
+ if (routeProperty === '*') return true
161
+ return routeProperty === requestProperty
122
162
  }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "make-fetch",
3
- "version": "2.3.4",
3
+ "version": "3.1.0",
4
4
  "description": "Implement your own `fetch()` with node.js streams",
5
5
  "main": "index.js",
6
+ "type": "module",
6
7
  "scripts": {
7
8
  "test": "node test",
8
9
  "lint": "standard --fix"
@@ -23,15 +24,8 @@
23
24
  "url": "https://github.com/RangerMauve/make-fetch/issues"
24
25
  },
25
26
  "homepage": "https://github.com/RangerMauve/make-fetch#readme",
26
- "dependencies": {
27
- "concat-stream": "^2.0.0",
28
- "fetch-headers": "^2.0.0",
29
- "fetch-request-body-to-async-iterator": "^1.0.3",
30
- "statuses": "^2.0.0",
31
- "web-streams-polyfill": "^3.0.0"
32
- },
33
27
  "devDependencies": {
34
28
  "standard": "^17.0.0",
35
- "tape": "^5.0.1"
29
+ "tape": "^5.6.1"
36
30
  }
37
31
  }
package/test.js CHANGED
@@ -1,37 +1,51 @@
1
- const test = require('tape')
1
+ import test from 'tape'
2
2
 
3
- const makeFetch = require('./')
3
+ import { makeFetch, makeRoutedFetch } from './index.js'
4
4
 
5
- test('Kitchen sink', async (t) => {
6
- try {
7
- const fetch = makeFetch(({ url }) => {
8
- return {
9
- statusCode: 200,
10
- headers: {
11
- url
12
- },
13
- data: intoAsyncIterable(url)
14
- }
15
- })
5
+ test('Basic makeFetch test', async (t) => {
6
+ const fetch = makeFetch(({ url }) => {
7
+ return {
8
+ status: 200,
9
+ statusText: 'OK',
10
+ headers: {
11
+ url
12
+ },
13
+ body: intoAsyncIterable(url)
14
+ }
15
+ })
16
16
 
17
- const toFetch = 'example'
17
+ const toFetch = 'example://hostname/pathname'
18
18
 
19
- const response = await fetch(toFetch)
19
+ const response = await fetch(toFetch)
20
20
 
21
- t.ok(response.ok, 'response was OK')
21
+ t.ok(response.ok, 'response was OK')
22
22
 
23
- const body = await response.text()
23
+ const body = await response.text()
24
24
 
25
- t.equal(body, toFetch, 'got expected response body')
25
+ t.equal(body, toFetch, 'got expected response body')
26
26
 
27
- t.equal(response.headers.get('url'), toFetch, 'got expected response headers')
27
+ t.equal(response.headers.get('url'), toFetch, 'got expected response headers')
28
28
 
29
- t.equal(response.statusText, 'OK', 'got expected status text')
29
+ t.equal(response.statusText, 'OK', 'got expected status text')
30
30
 
31
- t.end()
32
- } catch (e) {
33
- t.error(e)
34
- }
31
+ t.end()
32
+ })
33
+
34
+ test('Basic router tests', async (t) => {
35
+ const { fetch, router } = makeRoutedFetch()
36
+
37
+ router.get('ipfs://localhost/ipfs/**', ({ url }) => {
38
+ return { body: url }
39
+ })
40
+
41
+ const toFetch = 'ipfs://localhost/ipfs/cidwould/gohere'
42
+ const response = await fetch(toFetch)
43
+
44
+ t.ok(response.ok, 'response was OK')
45
+
46
+ const body = await response.text()
47
+
48
+ t.equal(body, toFetch, 'got expected body')
35
49
  })
36
50
 
37
51
  async function * intoAsyncIterable (data) {
package/index.d.ts DELETED
@@ -1,74 +0,0 @@
1
- declare module 'make-fetch' {
2
- export interface NormalizedRequest {
3
- url: string
4
- headers: RawHeaders
5
- method: string
6
- body: AsyncIterableIterator<Uint8Array>
7
- referrer?: string
8
-
9
- // Hard to add types for. 😂
10
- signal?: any
11
- }
12
-
13
- export interface HandlerResponse {
14
- statusCode?: number
15
- statusText?: string
16
- headers?: RawHeaders
17
- data?: AsyncIterableIterator<Uint8Array | string>
18
- }
19
-
20
- export interface RawHeaders {
21
- [name: string]: string | undefined
22
- }
23
-
24
- // TODO: Support actual fetch Headers
25
- export interface Headers {
26
- append(name: string, value: string) : void
27
- delete(name: string) : void
28
- entries() : IterableIterator<[string, string]>
29
- get(name: string): string | undefined
30
- has(name: string) : boolean
31
- keys() : IterableIterator<string>
32
- set(name: string, value: string) : void
33
- values() : IterableIterator<string>
34
- }
35
-
36
- export interface Request {
37
- // This is kind of a pain to document
38
- session?: any
39
- signal?: any
40
-
41
- url?: string
42
- headers?: Headers | RawHeaders
43
- method?: string,
44
- body?: string | Uint8Array | AsyncIterableIterator<Uint8Array|string>,
45
- referrer?: string,
46
- }
47
-
48
- export interface Body {
49
- // TODO: Fill this in
50
- }
51
-
52
- export interface Response {
53
- url: string
54
- headers: Headers
55
- status: number
56
- statusText: string
57
- ok: boolean
58
- useFinalURL: true
59
- body: Body
60
- arrayBuffer() : Promise<ArrayBuffer>
61
- text(): Promise<string>
62
- json(): Promise<any>
63
- }
64
-
65
- export interface Fetch {
66
- (request: string | Request, info? : Request): Promise<Response>
67
- }
68
-
69
- export interface FetchHandler {
70
- (request: NormalizedRequest): Promise<HandlerResponse>
71
- }
72
-
73
- export default function makeFetch(handler: FetchHandler) : Fetch
74
- }
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2018",
4
- "moduleResolution": "node",
5
- "module": "commonjs",
6
- "lib": [
7
- "dom",
8
- "es2018"
9
- ]
10
- }
11
- }