GithubHelp home page GithubHelp logo

Comments (14)

markusahlstrand avatar markusahlstrand commented on May 19, 2024 1

Thanks for the reply!

The use case I have right now is that I would like to log the responses to a S3 firehose, which I think should work fine with doing a then/catch on the handler like you showed above. It would be nifty to be able to use middlewares like in koa-router, but for the time being it's not something I need.

from itty-router.

kwhitley avatar kwhitley commented on May 19, 2024 1

Here is what I do. Not ideal, but works. Having real middlewares would be awesome.

I mean, by its definition, middleware is simply anything that operates in the middle (in this case of a request and the eventual response/return). Any handler is technically middleware, the same as in Express. The only things differentiating itty's middleware from Express middleware is:

  • Express requires an explicit next() call to continue to the next handler, vs itty that simply continues the handler chain until something returns. Both have pros/cons. I chose the itty flow because I opt for brevity/simplicity over explicit calls, and the end code gets to look much cleaner as a result. It makes it slightly more difficult to incrementally "assemble" a response, as a consequence.
  • Express passes both the request/response by default through the chain. This encourages you to assemble the response as you go through the middleware/handler chain. You could also do this with itty very easily by just passing in a Response (or more ideally some Response-like object that can construct a Response at the very end) after the request. The only thing itty expects is that the first param to any handler is a Request-like object. That's it. Want to more closely match an Express param pattern? You can!
import { ResponseMaker } from './somewhere'

// all handlers take the arguments you pass to the handle, in that order... so it's easy to change things up
const middleware = (req, res) => {
  res.status(400) // let's pretend this is how we modify the response-like object
}

const router = Router()

router
  .all('*', middleware)
  .get('/foo', (req, res) => {
    // res.status === 400 from the middleware
        
    return res.json({ error: 'Bad Request' }) // let's pretend this returns a JSON Response
  })
  .get('*', (req, res) => res.status(404).end())

// we change the default signature of handlers this easily...
export default {
  fetch: (req, ...other) => router.handle(req, new ResponseMaker(), ...other)
}

from itty-router.

markusahlstrand avatar markusahlstrand commented on May 19, 2024

After testing a few different approaches I have a slightly different proposal. Adding the next parameter would be a breaking change which might not be ideal even though it's in my opinion would be the cleanest solution.

Another solution would be to allow the middleware to return a function that is executed on the returned response. This way a middleware can modify the response before it's sent to the client:

async function middleware(request) {
  const startAt = new Date();
  return async (err, response) => {
    // Log the duration of the call
    console.log(`Duration: ${new Date() - startAt}`)
    
    // Not sure if the headers are immutable? 
    response.headers.foo = 'bar';
    return response;
  } 
}

If any errors are return you could use this pattern to write error handlers as well.

Not sure if this could achieved with the Proxy functionality? Guess it would need to be part of the actual router?

from itty-router.

kwhitley avatar kwhitley commented on May 19, 2024

Hey @markusahlstrand - currently you can do one of the following:

  1. Modify the response after router.handle returns one:
router
  .handle(request)
  .then(response => {
    // do something to the response, like inject CORS headers, etc
    return response
  })
  .catch(err => console.log(err))
  1. Build a response incrementally, using the request (or any additional arg) as the carrier agent between middleware/handlers.

The current (request, ...args) signature, while different than (request, response, next) of Express, is actually one of the reasons itty works out of the box with Cloudflare's module syntax, as additional params (specifically the env) can easily be passed along through the handler chain.

from itty-router.

kwhitley avatar kwhitley commented on May 19, 2024

That said, itty is also incredibly flexible thanks to this freeform signature. If you wanted to emulate something more like Express, you could simply do something like this:

const router = Router()

router
  .get('*', (request, response) => {
    response.status = 204 // modify response, but exit without returning
  })
  .all('*', (request, response) => response) // return response downstream once done building

// and where you call the handle method elsewhere, just pass in a fresh response... 
// middleware/handlers all receive any params sent to the handle function.
router.handle(request, new Response())

from itty-router.

jamesarosen avatar jamesarosen commented on May 19, 2024

Here's a use-case that is easily modeled with next:

export default async function attachSessionToCachedResponse(request, next) {
  // get the edge-cached HTML and the session info simultaneously:
  const sessionRequest = new Request('https://api.example.com', { headers: request.headers })
  const [cachedResponse, sessionResponse] = Promsie.all([ next(), fetch(sessionRequest) ])

  // attach the session cookies to a mutable copy of the HTML response:
  const responseWithSession = new Response(cachedResponse.body, cachedResponse)
  responseWithSession.headers.set('Set-Cookie', sessionResponse.headers.get('Set-Cookie'))
  return responseWithSession
}

That's certainly possible with the current framework, but it's a little fragile:

router.get('*', (request, response) => {
  // get the session asynchronously:
  const sessionRequest = new Request('https://api.example.com', { headers: request.headers })
  const sessionResponsePromise = fetch(sessionRequest)

  // teach the response how to attach the session once it's resolved:
  response.attachSession = async () => {
    const sessionResponse = await sessionResponsePromise
    const responseWithSession = new Response(this.body, this)
    responseWithSession.headers.set('Set-Cookie', this.headers.get('Set-Cookie'))
    return responseWithSession
  }

  // don't return so the request flows upstream
}

router
  .handle(request, new Response())
  .then(response => return response.attachSession()) // attach the session

from itty-router.

danbars avatar danbars commented on May 19, 2024

Here is what I do.
Not ideal, but works.
Having real middlewares would be awesome.

addEventListener('fetch', event => {
  const mainRouter = Router()
  mainRouter
    .all('/*', logger({
      logDnaKey: LOG_DNA_KEY,
      host: LOG_DNA_HOSTNAME,
      appName: LOG_DNA_APPLICATION_NAME}))
    .all('/*', requestTimer)
    .all('/v1/*', someOtherHandler)
    .all('*', missingHandler) 

  event.respondWith(
    mainRouter
      .handle(event.request, event)
      .then(async response => {
        await finalize(event, response)
        return response
      })
      .catch(async error => {
        return await errorHandler(error, event)
      })
  )
})

// in this method I do all the response middleware.
//not as nice as real middlewares because all concerns are in one place, but it does the work
//note that your error handler also has to call this method
async function finalize(event, response) {
  event.request.timer.end() //this one measures request handling time
  response.headers.set('X-Timer', event.request.timer.total())
  response.headers.set('X-RequestId', event.request.requestId)
  const origin = event.request.headers.get('origin') // handle CORS
  if (origin && ['https://allowed.origin.com', 'http://localhost:9292'].includes(origin)) {
    response.headers.set('Access-Control-Allow-Origin', origin)
    response.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, DELETE, PUT, PATCH')
    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    response.headers.set('Access-Control-Allow-Credentials', 'true')
  }
  event.waitUntil( //write to logger. I use logDNA
    flushLog(event.request)
  )
}

from itty-router.

Moser-ss avatar Moser-ss commented on May 19, 2024

Hey @kwhitley I saw in several comments about the topic of handling responses the following code snippet

router
  .handle(request)
  .then(response => {
    // do something to the response, like inject CORS headers, etc
    return response
  })
  .catch(err => console.log(err))

But I am struggling to adapt that to my implementation

router.all('/api/*', endpoints.handle)

because I am not sure how to chain the handle request in this case

from itty-router.

danbars avatar danbars commented on May 19, 2024

@Moser-ss your line is just the configuration of the router, it doesn't actually handle the request.
After that you have to do router.handle(request)
To this one you can chain .then(response) just as in the snippet that you showed

from itty-router.

Moser-ss avatar Moser-ss commented on May 19, 2024

Thanks for the tip, so assuming this setup

const router = Router();

router.all('/api/*', endpoints.handle);
router.all('/webhooks/*', webhooks.handle);
router.all('/websocket/*', websocket.handle);
router.all('*', () =>
  jsonResponse(
    {
      error: "Resource doesn't exist.",
    },
    404
  )
);

export default {
  fetch: router.handle,
  scheduled: async (
    event: Event,
    env: EnvInterface,
    ctx: ExecutionContext
  ): Promise<void> => {
    ctx.waitUntil(cronTrigger(env));
  },
};

I will need to create a new function where I call the router.handle(request, env,ctx).then(response) but I was thinking, technically I can still use async-await
example in pseudo-code

async function main(request, env,ctx) {
 const response = await router.handle(request, env,ctx)
// manipulate response object
return response
}

the only downside of this approach is the fact the response handler will be apply to all routes

from itty-router.

danbars avatar danbars commented on May 19, 2024

from itty-router.

danbars avatar danbars commented on May 19, 2024

The feature that I'm missing from express style is the ability to do something at the beginning before all other middleware, and then at the end after all middleware ran and returned their response.
Examples:

  • at the beginning I'm setting a timer, and at the end I'm measuring handling time and adding it as header or writing it to Graphana
  • At the beginning I'm creating log-context object and capturing console.log, and the end I'm flushing all console.log output to Cloudwatch logger.

With your method of using ResponseMaker once a middleware returns you will not reach the "after" middlewares.

It is a nice trick, but doesn't solve all cases

from itty-router.

danbars avatar danbars commented on May 19, 2024

Something that I just thought about, but didn't try, is to create another instance of Itty Router that will handle the response middlewares.

event.respondWith(
    mainRouter
      .handle(event.request, event)
      .then(response => {
        return responseRouter(event.request, response)
      })
      .catch(async error => {
        return await errorHandler(error, event)
      })
  )

This way you can configure responseRouter with multiple handlers per method/path.

from itty-router.

markusahlstrand avatar markusahlstrand commented on May 19, 2024

I ended up with a solution where the handler can return a function that in turn returns a promise, which works well for me but it is unfortunately a pretty large change to the router and not really in line with the simplicity philosophy of this project. If you're interested I can share this solution @danbars

from itty-router.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.