question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Solving the withParams global middleware issue

See original GitHub issue

Currently a withParams middleware that targets request.params cannot be injected upstream as global middleware, because at this point, params would have not been set (to be spread into the request).

import { Router } from 'itty-router'
import { withParams } from 'itty-router-extras'

const router = Router()

// currently works
router
  .get('/:id', withParams, ({ id }) => new Response(id))
  
// currently fails
router
  .all('*', withParams)
  .get('/:id', ({ id }) => new Response(id))

Proposed Solution

  1. Pass request.proxy || request through to handlers… middleware can inject a proxy (if needed) to trigger this. This costs about ~5 bytes.
  2. Middleware can create a Proxy around the request on request.proxy and effectively watch the request (for loggers and such), or modify how they read things (e.g. withParams intercepts “gets” to the request proxy and find the values in the request.params)

After (Line 12)

image

This allows a middleware upstream to watch any change on the request itself, including (but not limited to) request.params, and fire appropriately. It also allows each middleware to proxy the existing proxy, meaning multiple middleware can watch independent request props without worrying about collisions.

With this, an upstream withParams could be defined as follows, then downstream changes to params would trigger the proxy (assigning params into the request).

image

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
kwhitleycommented, Mar 30, 2021

🤯 I still can’t believe you came up with this so fast…

0reactions
kwhitleycommented, Mar 31, 2021

Where we’ve left it for the night… by simply passing request.proxy || request into the handle() calls, we don’t have to explicitly set request.proxy upfront in itty, but simply honor downstream injections of it. This does two things:

  1. Keeps the core code short and sweet.
  2. Allows downstream proxying of the request, by simply adding a request.proxy = new Proxy(request, { whatever }).

This is important, because it can not only allow things like withParams to modify the get into the request [proxy] that the handlers see, but things can actually watch for future changes by modifying the set path. This could be useful for loggers that are set up to log downstream changes to the request, for instance!

Example test case showing some of this power in action:

// mmmmmmmmmmmmm the following utility functions would be relocated to itty-router-extras mmmmmmmm

const notProxy = prop => prop !== 'proxy'

// similar to watch, but takes a predicate function (receives prop, value, and request)
// to determine if fn should fire (with value and request)
const dynamicWatch = (fn, predicate = notProxy) => request => {
  request.proxy = new Proxy(request, {
    set: (obj, prop, value) => {
      obj[prop] = value
      predicate(prop, obj) && fn(value, prop, request)

      return true
    }
  })
}

// 1. this executes a watcher function when the prop changes on request
// 2. curried signature to allow the product to BE middleware, if desired
// (would export this from itty-router-extras)
const watch = (prop, fn) => dynamicWatch(fn, key => key === prop)

// predicate comes last again, to default to all things flowing through this, if not defined,
// rather than the awkward () => true syntax for using everywhere
const dynamicRetrieve = (fn, predicate = notProxy) => request => {
  request.proxy = new Proxy(request, {
    get: (obj, prop) => predicate(prop, obj)
                        ? fn(prop, request)
                        : obj[prop]
  })
}

// 1. this allows for modifying reads from the request
// 2. curried signature to allow the product to BE middleware, if desired
// (would export this from itty-router-extras)
const retrieve = (prop, fn) => dynamicRetrieve(key => key === prop, fn)

// now for the test!
  describe('request.proxy embedding', () => {
    it('allows for upstream request-watching', async () => {
      const router = Router()
      const handler = jest.fn(req => req.id)
      const logger = jest.fn(v => v)
      const simplelogger = jest.fn(user => user.id)
      const dynamicLogger = jest.fn(user => `${user.id}*`)
      const everythingLogger = jest.fn((value, prop) => console.log(prop, 'changed to', value))

      // without a predicate, a dynamic watcher will fire on any request update
      const watchEverything = dynamicWatch(everythingLogger)

      // longhand for watch('user', fn), using the predicate to target props
      const withDynamicWatch = dynamicWatch(
        dynamicLogger,
        prop => prop === 'user',
      )

      // if you need access to the request, you could do it this way
      const withUserTracking = request => {
        request.foo = 'bar' // this is here to see if the watchEverything middleware fires twice
        watch('user', user => logger({ user, url: request.url }))(request)
      }

      // but this is simpler when watching a single prop
      const withSimpleUserTracking = watch('user', simplelogger)

      // similar syntax for retrieving [dynamic] props.  No predicate 2nd param means everything
      // passes through this function.
      const withParams = dynamicRetrieve((prop, request) =>
                                    request.params && request.params[prop]
                                    ? request.params[prop]
                                    : request[prop])

      // just embeds user in the request... other middleware will fire as a result!
      const withUser = request => {
        request.user = { id: '15' }
      }

      router
        .all('*', watchEverything, withUserTracking, withDynamicWatch, withSimpleUserTracking, withUser, withParams)
        .get('/:id', handler)

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

      expect(handler).toHaveReturnedWith('13')
      expect(logger).toHaveReturnedWith({ url: 'https://example.com/13', user: { id: '15' } })
      expect(simplelogger).toHaveReturnedWith('15')
      expect(logger).toHaveBeenCalledTimes(1)
      expect(everythingLogger).toHaveBeenCalledTimes(2)
      expect(dynamicLogger).toHaveReturnedWith('15*')
    })
  })
Read more comments on GitHub >

github_iconTop Results From Across the Web

filesystem middleware doesn't work for routes with params #834
When using the filesystem middleware for a route that contains parameters, like /:myparam/web , it doesn't work. Code snippet. The following ...
Read more >
Inspecting constructor parameters inside an autofac middleware
I'm using Autofac as my DI container. What I'd like is to have Autofac resolve the dependencies of class C using the parameter...
Read more >
Middleware - Laravel - The PHP Framework For Web Artisans
The withoutMiddleware method can only remove route middleware and does not apply to global middleware. Middleware Groups. Sometimes you may want to group ......
Read more >
5.x API - Express.js
Returns middleware that only parses JSON and only looks at requests where the Content-Type header matches the type option. This parser accepts any...
Read more >
[Solved]-Rails issue with params hash-ruby - appsloveworld
[Solved]-Rails issue with params hash-ruby. Search. score:1. Accepted answer. Modifying your code a little bit to get @people.each_with_index do |p,i| ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found