-
-
Notifications
You must be signed in to change notification settings - Fork 376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consider adding a finally
phase.
#622
Comments
Thank you for the detailed use case. I ran into this years ago when I first started using middy. Proper ordering of ones' middlewares can resolve most I thought about adding in Have you tried inline middlewares? If not checkout the docs for more details. This might work: normalizeResponse = normalizeResponse()
jsonToString = jsonToString()
middy(handler)
.after(normalizeResponse.after)
.after(jsonToString.after)
.use(errorMessage())
.onError(jsonToString.after)
.onError(normalizeResponse.after)
.onError(handler => {
return handler.response
})
// after: jsonToString -> normalizeResponse
// onError: errorMessage -> jsonToString -> normalizeResponse -> inlineMiddleware This should give you the control you need. What do you think of this approach? or do we need to think of a way to group middlewares the need to be run in the same order for |
Hi @willfarrell. Thank you for your response! Let me address your points inline.
Most is the keyword here. Although I would argue it's not even that many. The example I presented in my OT cannot be resolved by reordering. Any time you have a middleware implementing both
I was taking a peek at that (thanks for sharing, BTW). I thought of a couple of comments.
.use(cors())
.use(jsonapi({ response })) Since
middy(handler)
.use(cors())
.use(httpError())
// Naming could be improved - maybe onCleanupError?
.onUncaughtError(err => log.error('Something unexpected happened', err)); That's it. I don't see the need for further complexity. Unless you were thinking of something else?
Thanks for pointing this out. I have, yes. I frankly don't see them as an alternative to the predicaments I'm facing. Just take a hard look at your proposed solution. Would you honestly say that's easy to read? Or maintain? Or put together in the first place? IMO, it looks more like forcing an awkward solution to navigating around the difficulties of the API, rather than the intended way of working with Here's how the same would look by having support for a middy(handler)
.use(normalizeResponse()) // implements `cleanup`
.use(json()) // implements `cleanup`
.use(error()); // implements `onError` Much simpler, nicer looking, does exactly what it's supposed to do every time - and, more importantly, it's more resilient to future changes and addition/removal of other middlewares in the chain. |
@willfarrell I went ahead and implemented a PoC for this in #623. It was actually pretty straightforward. I wasn't very thorough about adding new tests or documentation as I wanted to run this by you first and hear your comments. |
Thanks for the detailed response. All great points. My favourite quote:
Wouldn't that be nice. I'll take a look at the PR and discuss with @lmammino if / when this is something we want to incorporate and what the cascading implications are throughout the codebase. I cannot guarantee it will be included in v2. |
So, we had a good discussion on this and came up with some early options. Change onError orderCreate a breaking change by reversing the order of the Add in a finally middleware stackAllow middleware to define a finally handler. This adds complexity and requires extra care and testing by the developer. Add
|
@willfarrell The first option sounds reasonable to me. And more inline with how middlewares should work within |
I agree that it makes more sense to have the const errorHandlerMiddleware = async (handler, next) => {
try {
await next();
} catch (e) {
handler.response = {
statusCode: e.statusCode || 500,
body: e.message
};
}
};
const loggerMiddleware = async (handler, next) => {
const startTime = performance.now();
try {
await next();
} finally {
const elapsed = performance.now() - startTime;
console.log(`request took ${elapsed} ms`);
}
}; |
@andrew0 As a Koa member and contributor, I'd like that very much. However, it'd mean a restructuring of |
Personally, I think that middy is still in its infancy, and 3.0 would break things anyway. We all could make a 3.0 alpha and try it in our own middy stacks, to see if it works in the wild or not. I use my own stack with very little of upstream middy due to #641 performance issues. |
I don't use any of the With regards to breaking |
Only in APIGatewayV1,
I do it too. The reason for that is that the stock versions consume ~250 ms of cold start time to do things I don't need. With zero impact and zero useless features more people would be able to use stock It would be nice if you could share the middlewares you use. Looking at real life middlewares could help develop better stock middlewares. I have some at https://github.com/nponeccop/serverless-openapi-templates/tree/master/validation-middy |
True. But still, API Gateway v1 is widely used and the added benefit of not consuming lambda runtime when validating makes the option a no-brainer. Users on v1 would/should see no benefit from
Would be nice to define and measure "zero impact". Obviously, that's just a gold standard or aspiration to strive for more than achievable goal. Something similar could be said about "useless features". What's useless to me might not be for others.
I don't have any public repos with examples, unfortunately. But here's a Gist of a |
Yes. As long as they set up the Also, some users of v1 might not be satisfied with the stock error messages, or need custom async validators which aren't portable and thus cannot be sensibly supported by AWS.
It isn't very hard to quantify. We just need to agree on something arbitrary. :-) I think as long as you can't do 2x better with handwritten code it's zero impact for many people. The problem is that current approach is 10x worse in
Yeah, a quick and dirty gist is fine. |
@nponeccop @andrew0 @willfarrell I took the liberty of opening #651 for us to start the conversation on how v3 should look like. Let me know what you think. |
The reverse ordering of onError is now included in the v3 branch and will be release later this month! Closing for now. |
I've been using
middy
and writing custom middlewares for it for some time now and I always find myself facing this problem.The "onion-like" scheme that
middy
follows when invoking middlewares makes it sobefore
andafter
hooks are called in inverted order. However, when an error is thrown, allafter
callbacks are ignored andonError
hooks are chained using the original declaration order. This would be fine if you consider youronError
flow to be completely isolated from yourafter
chain - but, in my experience, most of the time, this is not the case. Logic like body serialization, adding custom headers, normalizing the response, etc. you'd still need to run regardless of whether an exception was raised or not. Getting that to work withmiddy
and its current API is not straightforward at all. If you have ever written a middleware that supports bothafter
andonError
phases, you have probably struggled with this by now.Here's a real life example. Consider the following middlewares:
json
: simply appliesJSON.stringify
to thehandler.response.body
. Definesafter
andonError
.error
: Builds an error response from a raised exception. DefinesonError
.normalizeResponse
: Ensures that the response conforms to the API Gateway{ body?, statusCode?, headers?, multiValueHeaders?, isBase64Encoded? }
expected shape. Definesafter
andonError
.On a successful lambda invocation (i.e.: no errors are thrown), the expected middleware execution order should be:
normalizeResponse
->json
(error
is not called). Which leads to the following setup:In case of an error, the correct order should now be
error
->normalizeResponse
->json
. But becausejson
andnormalizeRespose
calls are swapped, this is incompatible with the declaration shown above.AFAIK, the only way to honour the middleware pattern and work around this situation is to create different middlewares that do the same thing on different phases and
.use
them in different orders.This completely defeats the purpose of having reusable logic encapsulated in middleware as units and quickly multiplies the number of middlewares needed to accomplish the same thing.
With all of the above said, I feel like
middy
would greatly benefit from having an extra phase that would always execute last, regardless of whether an error was thrown or not. This would clearly emulate the classictry/catch/finally
control flow.A setup like that could simplify building middlewares, help decoupling logic and encourage reusability. More often than not, when a middleware declares both
after
andonError
hooks pointing to the same function, what it's really trying to achieve is to execute that piece of logic no matter what the exit conditions are. Unfortunately,middy
turns that into an irreconcilable issue.Moreover, many of the middlewares currently being published under
@middy/*
, such as@middy/http-cors
and@middy/http-security-headers
to mention a couple, suffer from the same symptoms outlined before. All of them could be remedied by moving theirafter
andonError
calls to a singlecleanup
.Any thoughts?
The text was updated successfully, but these errors were encountered: