Comments (14)
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.
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.
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.
Hey @markusahlstrand - currently you can do one of the following:
- 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))
- 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.
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.
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.
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.
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.
@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.
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.
from itty-router.
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 return
s you will not reach the "after" middlewares.
It is a nice trick, but doesn't solve all cases
from itty-router.
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.
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)
- Support `withContent` without `content-type` header HOT 2
- Invalid Regular Expression after fetch HOT 2
- Middleware functions don't seem to typecheck correctly for `request` HOT 5
- Can I list all routes defined by a router HOT 4
- How to Use Itty Router with Next.js? HOT 1
- Typescript router "Additional types" are ignored HOT 10
- Setting up cron trigger? HOT 2
- cors functions are stateful HOT 8
- Nested routers ergonomics HOT 8
- createCors().corsify eats additional Set-Cookie headers HOT 3
- Does `json()` support `headers` in options? HOT 4
- `withContent` returns a 500 when no body is sent HOT 1
- CORS issue, no preflight request shown HOT 6
- PROPOSAL: CORS HOT 4
- multiple set-cookie headers are removed by corsify
- Exception thrown by withContent HOT 5
- Corsify override http error code. HOT 3
- typescript compilation error when using itty-router 5.0.5 HOT 3
- Uncaught TypeError: (src_default.middleware ?? []) is not iterable HOT 3
- Issue with CORS when response is cached HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from itty-router.