Skip to content
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

Add error handling middleware #126

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

gayathrisairam
Copy link

Motivation

Implementation of Improved error handling proposal

Modifications

Added HTTPResponseConvertible protocol
Added ErrorHandlingMiddleware that converts errors confirming to HTTPResponseConvertible to a HTTP response.

Result

The new middleware is an opt-in middleware. So there won't be any change to existing clients.
Clients who wish to have OpenAPI error handling can include the new error middleware in their application.

Test Plan

Added E2E tests to test the new middleware.

Copy link
Collaborator

@simonjbeaumont simonjbeaumont left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking this up @gayathrisairam!

I've left some initial comments / questions, and a note on how you can run the linters locally.

Comment on lines 71 to 80
public protocol HTTPResponseConvertible {
/// HTTP status to return in the response
var httpStatus: HTTPResponse.Status { get }

/// (Optional) Headers to return in the response
var httpHeaderFields: HTTPTypes.HTTPFields { get }

/// (Optional) The body of the response to return
var httpBody: OpenAPIRuntime.HTTPBody? { get }
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go and double check the proposal myself but I thought we were going to make these all optional? Right now, we have all three flavours:

  • httpStatus is non-optional, with no default implementation;
  • httpHeaderFields is non-optional, but has a default implementation; and
  • httpBody is optional, with a default implementation.

IMO, if we're providing a default implementation in a protocol extension, we should not use optional for the requirement.

I'll go double-check the proposal again, but, regardless of API spelling, is this the semantics we wanted here?

Specifically, if someone adds the ErrorHandlingMiddleware, don't we want it transform errors that do not conform into 500 and, in debug mode, possibly include something in the header or body?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, if we're providing a default implementation in a protocol extension, we should not use optional for the requirement.

I'm not sure I agree, I think it's good the way it is - header fields is a container that can be empty, so it's non-optional and defaults to empty in the protocol extension. The body, however, is a stateful async sequence, and the middleware API allows returning nil, so I think it should be optional and default to nil in the protocol extension.

Specifically, if someone adds the ErrorHandlingMiddleware, don't we want it transform errors that do not conform into 500 and, in debug mode, possibly include something in the header or body?

Yes I also caught this one, I believe here we agreed to always return a response, and use 500 for unconformed errors.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, if we're providing a default implementation in a protocol extension, we should not use optional for the requirement.

I'm not sure I agree, I think it's good the way it is - header fields is a container that can be empty, so it's non-optional and defaults to empty in the protocol extension. The body, however, is a stateful async sequence, and the middleware API allows returning nil, so I think it should be optional and default to nil in the protocol extension.

Right I think the way it's currently structured and documented is conflating two things. The optionality of whether a user needs to provide an override, and the optionality of the underlying field. We should have these match the target response type in terms of optionality, which, as you point out, they do already. Concretely, the HTTPResponse has non-optional status and headers (although headers may be empty), and an optional body.

I think what's confusing then is the use of "(Optional)" in the documentation comments for the protocol requirements. How about we remove that term from the documentation and provide defaults for everything in the protocol extension to make it clear, including httpBody, which will default to nil and make it clear what the default is in the protocol level documentation?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I will convert undocumented errors to a 500 response.
What do we want to do with header and body fields? Should we preserve the additional information in ServerError.underlyingError in the body as an application/json?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we preserve the additional information in ServerError.underlyingError in the body as an application/json?

I'd personally hold off on doing that, as it has the risk of leaking sensitive information in the error without the user explicitly opting into that behavior. I'd leave that up to the user in their HTTPResponseConvertible to send back as much information as they want, and for unconformed errors to only send 500, without any other headers/body.

Users can always add a logging middleware to at least see the error in logs, and this more conservative default behavior could further motivate users to conform their errors to the protocol and make an explicit decision about the response.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's precedent for this being done in debug and not in release. And I'm personally in favour of having this behaviour. Not having it is quite an annoying developer experience IMO.

I'd also propose making it configurable and defaulting to off. But it should be a backwards compatible change so happy to defer it altogether for now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to leave it out of this PR, however I would be supportive of a subsequent PR adding it as an explicit option. I really dislike the behavior being controlled by debug vs release.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, @gayathrisairam feel free to break this out into a new GitHub issue where we can flesh out how this would look.

Copy link
Collaborator

@czechboy0 czechboy0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great overall, added a few suggestions for more polish, mainly in the doc comments.

Copy link
Collaborator

@czechboy0 czechboy0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes, added a few more minor suggestions as part of the final pass

@czechboy0
Copy link
Collaborator

@gayathrisairam please rerun the formatter and unit tests, those are failing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants