-
Notifications
You must be signed in to change notification settings - Fork 5
Improve error handling #76
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
base: main
Are you sure you want to change the base?
Changes from all commits
634287f
4506007
6f886e2
cb84c52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -213,82 +213,101 @@ public struct NIOHTTPServer: HTTPServer { | |
| } | ||
| } | ||
|
|
||
| /// Handles a single HTTP request. | ||
| /// | ||
| /// - Note: Errors do not propagate to the caller. When an error occurs, it is logged and the channel is closed. | ||
| /// | ||
| /// - Parameters: | ||
| /// - channel: The async channel to read the request from and write the response to. | ||
| /// - handler: The request handler. | ||
| func handleRequestChannel( | ||
| channel: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, | ||
| handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter> | ||
| ) async throws { | ||
| ) async { | ||
| do { | ||
| try await channel | ||
| .executeThenClose { inbound, outbound in | ||
| var iterator = inbound.makeAsyncIterator() | ||
|
|
||
| let httpRequest: HTTPRequest | ||
| switch try await iterator.next() { | ||
| case .head(let request): | ||
| httpRequest = request | ||
| case .body: | ||
| self.logger.debug("Unexpectedly received body on connection. Closing now") | ||
| outbound.finish() | ||
| return | ||
| case .end: | ||
| self.logger.debug("Unexpectedly received end on connection. Closing now") | ||
| outbound.finish() | ||
| return | ||
| case .none: | ||
| self.logger.trace("No more requests parts on connection") | ||
| return | ||
| } | ||
| try await channel.executeThenClose { inbound, outbound in | ||
| var iterator = inbound.makeAsyncIterator() | ||
|
|
||
| let nextPart: HTTPRequestPart? | ||
| do { | ||
| nextPart = try await iterator.next() | ||
| } catch { | ||
| self.logger.error( | ||
| "Error thrown while advancing the request iterator", | ||
| metadata: ["error": "\(error)"] | ||
| ) | ||
| throw error | ||
| } | ||
|
|
||
| let readerState = HTTPRequestConcludingAsyncReader.ReaderState() | ||
| let writerState = HTTPResponseConcludingAsyncWriter.WriterState() | ||
|
|
||
| do { | ||
| try await handler.handle( | ||
| request: httpRequest, | ||
| requestContext: HTTPRequestContext(), | ||
| requestBodyAndTrailers: HTTPRequestConcludingAsyncReader( | ||
| iterator: iterator, | ||
| readerState: readerState | ||
| ), | ||
| responseSender: HTTPResponseSender { response in | ||
| try await outbound.write(.head(response)) | ||
| return HTTPResponseConcludingAsyncWriter( | ||
| writer: outbound, | ||
| writerState: writerState | ||
| ) | ||
| } sendInformational: { response in | ||
| try await outbound.write(.head(response)) | ||
| } | ||
| ) | ||
| } catch { | ||
| logger.error("Error thrown while handling connection: \(error)") | ||
| if !readerState.wrapped.withLock({ $0.finishedReading }) { | ||
| logger.error("Did not finish reading but error thrown.") | ||
| // TODO: if h2 reset stream; if h1 try draining request? | ||
| } | ||
| if !writerState.wrapped.withLock({ $0.finishedWriting }) { | ||
| logger.error("Did not write response but error thrown.") | ||
| // TODO: we need to do something, possibly just close the connection or | ||
| // reset the stream with the appropriate error. | ||
| let httpRequest: HTTPRequest | ||
| switch nextPart { | ||
| case .head(let request): | ||
| httpRequest = request | ||
| case .body: | ||
| self.logger.debug("Unexpectedly received body on connection. Closing now") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have something like a |
||
| outbound.finish() | ||
| return | ||
| case .end: | ||
| self.logger.debug("Unexpectedly received end on connection. Closing now") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have something like a |
||
| outbound.finish() | ||
| return | ||
| case .none: | ||
| self.logger.trace("No more requests parts on connection") | ||
| return | ||
| } | ||
|
|
||
| let readerState = HTTPRequestConcludingAsyncReader.ReaderState() | ||
| let writerState = HTTPResponseConcludingAsyncWriter.WriterState() | ||
|
|
||
| do { | ||
| try await handler.handle( | ||
| request: httpRequest, | ||
| requestContext: HTTPRequestContext(), | ||
| requestBodyAndTrailers: HTTPRequestConcludingAsyncReader( | ||
| iterator: iterator, | ||
| readerState: readerState | ||
| ), | ||
| responseSender: HTTPResponseSender { response in | ||
| try await outbound.write(.head(response)) | ||
| return HTTPResponseConcludingAsyncWriter( | ||
| writer: outbound, | ||
| writerState: writerState | ||
| ) | ||
| } sendInformational: { response in | ||
| try await outbound.write(.head(response)) | ||
| } | ||
| throw error | ||
| ) | ||
| } catch { | ||
| if !readerState.wrapped.withLock({ $0.finishedReading }) { | ||
| self.logger.error("Did not finish reading but error thrown.") | ||
| // TODO: if h2 reset stream; if h1 try draining request? | ||
| } | ||
|
|
||
| // TODO: handle other state scenarios. | ||
| // For example, if we're using h2 and we didn't finish reading but we wrote back | ||
| // a response, we should send a RST_STREAM with NO_ERROR set. | ||
| // If we finished reading but we didn't write back a response, then RST_STREAM | ||
| // is also likely appropriate but unclear about the error. | ||
| // For h1, we should close the connection. | ||
| if !writerState.wrapped.withLock({ $0.finishedWriting }) { | ||
| self.logger.error("Did not write response but error thrown.") | ||
| // TODO: we need to do something, possibly just close the connection or | ||
| // reset the stream with the appropriate error. | ||
| } | ||
|
|
||
| // Finish the outbound and wait on the close future to make sure all pending | ||
| // writes are actually written. | ||
| outbound.finish() | ||
| try await channel.channel.closeFuture.get() | ||
| throw error | ||
| } | ||
|
|
||
| // TODO: handle other state scenarios. | ||
| // For example, if we're using h2 and we didn't finish reading but we wrote back | ||
| // a response, we should send a RST_STREAM with NO_ERROR set. | ||
| // If we finished reading but we didn't write back a response, then RST_STREAM | ||
| // is also likely appropriate but unclear about the error. | ||
| // For h1, we should close the connection. | ||
|
|
||
| // Finish the outbound and wait on the close future to make sure all pending | ||
| // writes are actually written. | ||
| outbound.finish() | ||
| try await channel.channel.closeFuture.get() | ||
| } | ||
| } catch { | ||
| self.logger.debug("Error thrown while handling connection: \(error)") | ||
| // TODO: We need to send a response head here potentially | ||
| self.logger.error("Error thrown while handling connection", metadata: ["error": "\(error)"]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. connectionID? |
||
|
|
||
| try? await channel.channel.close() | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't we just use
withThrowingDiscardingTaskGrouphere? The only error we're throwing is the one coming from iteratinginbound.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is primarily to make it explicit that if you call a throwing function from within the body of the task group, the error must be handled directly rather than letting it propagate upwards and resulting in active tasks in the group being cancelled.
Currently iterating on
inboundis the only source of a potential error, but if this was awithThrowingDiscardingTaskGroupthen in the future we could inadvertently introduce a call to a throwing function without the compiler warning us.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we add some comment here (and in the other func) explaining this rationale? I am not convinced just doing things this way would prevent a future change from changing the behaviour unless it's more documented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, addressed in commit cb84c52.