Skip to content

Commit

Permalink
Merge 4267356 into 2452057
Browse files Browse the repository at this point in the history
  • Loading branch information
kwhitley committed Mar 31, 2021
2 parents 2452057 + 4267356 commit c92becb
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 12 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ module.exports = {
'no-loss-of-precision': 'error',
'no-mixed-operators': 'error',
'no-mixed-requires': 'error',
'no-multi-assign': 'error',
'no-multi-assign': 'off',
'no-multi-spaces': 'error',
'no-multi-str': 'error',
'no-multiple-empty-lines': 'error',
Expand Down Expand Up @@ -186,7 +186,7 @@ module.exports = {
'no-underscore-dangle': 'error',
'no-unmodified-loop-condition': 'error',
'no-unneeded-ternary': 'error',
'no-unused-expressions': 'error',
'no-unused-expressions': 'off',
'no-use-before-define': 'error',
'no-useless-backreference': 'error',
'no-useless-call': 'error',
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Changelog
Until this library makes it to a production release of v1.x, **minor versions may contain breaking changes to the API**. After v1.x, semantic versioning will be honored, and breaking changes will only occur under the umbrella of a major version bump.

- **v2.3.0** - feature: request handler will be passed request.proxy (if found) or request (if not) - allowing for middleware magic downstream...
- **v2.2.0** - feature: base path (option) is now included in the route param parsing...
- **v2.1.1** - fix: should now be strict-mode compatible
- **v2.1.0** - now handles the problematic . character within a param (e.g. /:url/action with /google.com/action)
Expand Down
92 changes: 83 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ npm install itty-router
```js
import { Router } from 'itty-router'

// create a router
const router = Router() // this is a Proxy, not a class
// let's define a generic error handler to catch any breaks
const genericErrorHandler = error =>
new Response(error.message || 'Mysterious Error', { status: 500 })

// now let's create a router (note the lack of "new")
const router = Router()

// GET collection index
router.get('/todos', () => new Response('Todos Index!'))
Expand All @@ -53,7 +57,9 @@ router.all('*', () => new Response('Not Found.', { status: 404 }))

// attach the router "handle" to the event handler
addEventListener('fetch', event =>
event.respondWith(router.handle(event.request))
event.respondWith(
router.handle(event.request).catch(genericErrorHandler)
)
)
```

Expand All @@ -67,7 +73,7 @@ addEventListener('fetch', event =>
- [x] [Base path](#nested-routers-with-404-handling) for prefixing all routes.
- [x] [Multi-method support](#nested-routers-with-404-handling) using the `.all()` channel.
- [x] Supports **any** method type (e.g. `.get() --> 'GET'` or `.puppy() --> 'PUPPY'`).
- [x] Extendable. Use itty as the internal base for more feature-rich/elaborate routers.
- [x] [Extendable](#extending-itty-router). Use itty as the internal base for more feature-rich/elaborate routers.
- [x] Chainable route declarations (why not?)
- [ ] Readable internal code (yeah right...)

Expand Down Expand Up @@ -98,13 +104,13 @@ router.get('/todos/:user/:item?', (req) => {
```

### 3. Handle Incoming Request(s)
##### `router.handle(request: Request)`
##### `async router.handle(request.proxy: Proxy || request: Request, ...anything else)`
Requests (doesn't have to be a real Request class) should have both a **method** and full **url**.
The `handle` method will then return the first matching route handler that returns something (or nothing at all if no match).

```js
router.handle({
method: 'GET', // optional, default = 'GET'
method: 'GET', // required
url: 'https://example.com/todos/jane/13', // required
})

Expand Down Expand Up @@ -141,6 +147,47 @@ GET /todos/jane?limit=2&page=1
*/
```

#### A few notes about this:
- **Error Handling:** By default, there is no error handling built in to itty. However, the handle function is async, allowing you to add a `.catch(error)` like this:

```js
import { Router } from 'itty-router'

// a generic error handler
const errorHandler = error =>
new Response(error.message || 'Server Error', { status: error.status || 500 })

// add some routes (will both safely trigger errorHandler)
router
.get('/accidental', request => request.that.will.throw)
.get('/intentional', () => {
throw new Error('Bad Request')
})

// attach the router "handle" to the event handler
addEventListener('fetch', event =>
event.respondWith(
router.handle(event.request).catch(errorHandler)
)
)
```
- **Extra Variables:** The router handle expects only the request itself, but passes along any additional params to the handlers/middleware. For example, to access the `event` itself within a handler (e.g. for `event.waitUntil()`), you could simply do this:

```js
const router = Router()

router.add('/long-task', (request, event) => {
event.waitUntil(longAsyncTaskPromise)

return new Response('Task is still running in the background!')
})

addEventListener('fetch', event =>
event.respondWith(router.handle(event.request, event))
)
```
- **Proxies:** To allow for some pretty incredible middleware hijacks, we pass `request.proxy` (if it exists) or `request` (if not) to the handler. This allows middleware to set `request.proxy = new Proxy(request.proxy || request, {})` and effectively take control of reads/writes to the request object itself. As an example, the `withParams` middleware in `itty-router-extras` uses this to control future reads from the request. It intercepts "get" on the Proxy, first checking the requested attribute within the `request.params` then falling back to the `request` itself.

## Examples

### Nested Routers with 404 handling
Expand Down Expand Up @@ -230,9 +277,36 @@ router.get('/todos/:id.:format?', request => {

return new Response(`Getting todo #${id} in ${format} format.`)
})
```
### Extending itty router
Extending itty is as easy as wrapping Router in another Proxy layer to control the handle (or the route registering). For example, here's the code to
ThrowableRouter in itty-router-extras... a version of itty with built-in error-catching for convenience.
```js
import { Router } from 'itty-router'

// a generic error handler
const errorHandler = error =>
new Response(error.message || 'Server Error', { status: error.status || 500 })

// and the new-and-improved itty
const ThrowableRouter = (options = {}) =>
new Proxy(Router(options), {
get: (obj, prop) => (...args) =>
prop === 'handle'
? obj[prop](...args).catch(errorHandler)
: obj[prop](...args)
})

// GET /todos/13 --> Getting todo #13 in csv format.
// GET /todos/14.json --> Getting todo #14 in json format.
// 100% drop-in replacement for Router
const router = ThrowableRouter()

// add some routes
router
.get('/accidental', request => request.that.will.throw) // will safely trigger error (above)
.get('/intentional', () => {
throw new Error('Bad Request') // will also trigger error handler
})
```
## Testing and Contributing
Expand All @@ -258,7 +332,7 @@ const Router = (o = {}) =>
r.query = Object.fromEntries(u.searchParams.entries())

for (let h of hs) {
if ((s = await h(r, ...a)) !== undefined) return s
if ((s = await h(r.proxy || r, ...a)) !== undefined) return s
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/itty-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Router = (options = {}) =>
request.query = Object.fromEntries(url.searchParams.entries())

for (let handler of handlers) {
if ((response = await handler(request, ...args)) !== undefined) return response
if ((response = await handler(request.proxy || request, ...args)) !== undefined) return response
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/itty-router.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,5 +432,21 @@ describe('Router', () => {
expect(req.a).toBe(originalA)
expect(req.b).toBe(originalB)
})

it('will pass request.proxy instead of request if found', async () => {
const router = Router()
const handler = jest.fn(req => req)
let proxy

const withProxy = request => {
request.proxy = proxy = new Proxy(request, {})
}

router.get('/foo', withProxy, handler)

await router.handle(buildRequest({ path: '/foo' }))

expect(handler).toHaveReturnedWith(proxy)
})
})
})

0 comments on commit c92becb

Please sign in to comment.