From c41599a6d6d8b11c729032bf8913e06f4171e0fb Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Sat, 9 Dec 2023 06:50:35 +0200 Subject: [PATCH] Asyncraises HTTP layer V3 (#482) * No Critical and Recoverable errors anymore. * Recover raiseHttpCriticalError() * Post-rebase fixes. * Remove deprecated ResponseFence and getResponseFence(). * HttpProcessCallback and 2. * Fix callback holder. * Fix test issue. * Fix backwards compatibility of `HttpResponse.state` field. --- chronos/apps/http/httpcommon.nim | 79 ++- chronos/apps/http/httpserver.nim | 779 +++++++++++++----------------- chronos/apps/http/multipart.nim | 121 ++--- chronos/apps/http/shttpserver.nim | 27 +- tests/testhttpclient.nim | 395 ++++++++------- tests/testhttpserver.nim | 231 +++++---- tests/testshttpserver.nim | 25 +- 7 files changed, 840 insertions(+), 817 deletions(-) diff --git a/chronos/apps/http/httpcommon.nim b/chronos/apps/http/httpcommon.nim index 3ebe3ca..0f5370a 100644 --- a/chronos/apps/http/httpcommon.nim +++ b/chronos/apps/http/httpcommon.nim @@ -43,30 +43,48 @@ const ServerHeader* = "server" LocationHeader* = "location" AuthorizationHeader* = "authorization" + ContentDispositionHeader* = "content-disposition" UrlEncodedContentType* = MediaType.init("application/x-www-form-urlencoded") MultipartContentType* = MediaType.init("multipart/form-data") type + HttpMessage* = object + code*: HttpCode + contentType*: MediaType + message*: string + HttpResult*[T] = Result[T, string] HttpResultCode*[T] = Result[T, HttpCode] + HttpResultMessage*[T] = Result[T, HttpMessage] - HttpDefect* = object of Defect HttpError* = object of AsyncError - HttpResponseError* = object of HttpError - code*: HttpCode - HttpCriticalError* = object of HttpResponseError - HttpRecoverableError* = object of HttpResponseError - HttpDisconnectError* = object of HttpError - HttpConnectionError* = object of HttpError HttpInterruptError* = object of HttpError - HttpReadError* = object of HttpError - HttpWriteError* = object of HttpError - HttpProtocolError* = object of HttpError - HttpRedirectError* = object of HttpError - HttpAddressError* = object of HttpError - HttpUseClosedError* = object of HttpError + + HttpTransportError* = object of HttpError + HttpAddressError* = object of HttpTransportError + HttpRedirectError* = object of HttpTransportError + HttpConnectionError* = object of HttpTransportError + HttpReadError* = object of HttpTransportError HttpReadLimitError* = object of HttpReadError + HttpDisconnectError* = object of HttpReadError + HttpWriteError* = object of HttpTransportError + + HttpProtocolError* = object of HttpError + code*: HttpCode + + HttpCriticalError* = object of HttpProtocolError # deprecated + HttpRecoverableError* = object of HttpProtocolError # deprecated + + HttpRequestError* = object of HttpProtocolError + HttpRequestHeadersError* = object of HttpRequestError + HttpRequestBodyError* = object of HttpRequestError + HttpRequestHeadersTooLargeError* = object of HttpRequestHeadersError + HttpRequestBodyTooLargeError* = object of HttpRequestBodyError + HttpResponseError* = object of HttpProtocolError + + HttpInvalidUsageError* = object of HttpError + HttpUseClosedError* = object of HttpInvalidUsageError KeyValueTuple* = tuple key: string @@ -127,6 +145,11 @@ func toString*(error: HttpAddressErrorType): string = of HttpAddressErrorType.NoAddressResolved: "No address has been resolved" +proc raiseHttpRequestBodyTooLargeError*() {. + noinline, noreturn, raises: [HttpRequestBodyTooLargeError].} = + raise (ref HttpRequestBodyTooLargeError)( + code: Http413, msg: MaximumBodySizeError) + proc raiseHttpCriticalError*(msg: string, code = Http400) {. noinline, noreturn, raises: [HttpCriticalError].} = raise (ref HttpCriticalError)(code: code, msg: msg) @@ -135,9 +158,6 @@ proc raiseHttpDisconnectError*() {. noinline, noreturn, raises: [HttpDisconnectError].} = raise (ref HttpDisconnectError)(msg: "Remote peer disconnected") -proc raiseHttpDefect*(msg: string) {.noinline, noreturn.} = - raise (ref HttpDefect)(msg: msg) - proc raiseHttpConnectionError*(msg: string) {. noinline, noreturn, raises: [HttpConnectionError].} = raise (ref HttpConnectionError)(msg: msg) @@ -152,7 +172,15 @@ proc raiseHttpReadError*(msg: string) {. proc raiseHttpProtocolError*(msg: string) {. noinline, noreturn, raises: [HttpProtocolError].} = - raise (ref HttpProtocolError)(msg: msg) + raise (ref HttpProtocolError)(code: Http400, msg: msg) + +proc raiseHttpProtocolError*(code: HttpCode, msg: string) {. + noinline, noreturn, raises: [HttpProtocolError].} = + raise (ref HttpProtocolError)(code: code, msg: msg) + +proc raiseHttpProtocolError*(msg: HttpMessage) {. + noinline, noreturn, raises: [HttpProtocolError].} = + raise (ref HttpProtocolError)(code: msg.code, msg: msg.message) proc raiseHttpWriteError*(msg: string) {. noinline, noreturn, raises: [HttpWriteError].} = @@ -178,6 +206,23 @@ template newHttpWriteError*(message: string): ref HttpWriteError = template newHttpUseClosedError*(): ref HttpUseClosedError = newException(HttpUseClosedError, "Connection was already closed") +func init*(t: typedesc[HttpMessage], code: HttpCode, message: string, + contentType: MediaType): HttpMessage = + HttpMessage(code: code, message: message, contentType: contentType) + +func init*(t: typedesc[HttpMessage], code: HttpCode, message: string, + contentType: string): HttpMessage = + HttpMessage(code: code, message: message, + contentType: MediaType.init(contentType)) + +func init*(t: typedesc[HttpMessage], code: HttpCode, + message: string): HttpMessage = + HttpMessage(code: code, message: message, + contentType: MediaType.init("text/plain")) + +func init*(t: typedesc[HttpMessage], code: HttpCode): HttpMessage = + HttpMessage(code: code) + iterator queryParams*(query: string, flags: set[QueryParamsFlag] = {}): KeyValueTuple = ## Iterate over url-encoded query string. diff --git a/chronos/apps/http/httpserver.nim b/chronos/apps/http/httpserver.nim index 3bbee0f..9646956 100644 --- a/chronos/apps/http/httpserver.nim +++ b/chronos/apps/http/httpserver.nim @@ -32,8 +32,7 @@ type ## Enable HTTP/1.1 pipelining. HttpServerError* {.pure.} = enum - InterruptError, TimeoutError, CatchableError, RecoverableError, - CriticalError, DisconnectError + InterruptError, TimeoutError, ProtocolError, DisconnectError HttpServerState* {.pure.} = enum ServerRunning, ServerStopped, ServerClosed @@ -41,11 +40,10 @@ type HttpProcessError* = object kind*: HttpServerError code*: HttpCode - exc*: ref CatchableError + exc*: ref HttpError remote*: Opt[TransportAddress] ConnectionFence* = Result[HttpConnectionRef, HttpProcessError] - ResponseFence* = Result[HttpResponseRef, HttpProcessError] RequestFence* = Result[HttpRequestRef, HttpProcessError] HttpRequestFlags* {.pure.} = enum @@ -61,15 +59,15 @@ type KeepAlive, Graceful, Immediate HttpResponseState* {.pure.} = enum - Empty, Prepared, Sending, Finished, Failed, Cancelled, Default + Empty, Prepared, Sending, Finished, Failed, Cancelled, ErrorCode, Default + + HttpProcessCallback* = + proc(req: RequestFence): Future[HttpResponseRef] {. + gcsafe, raises: [].} - # TODO Evaluate naming of raises-annotated callbacks HttpProcessCallback2* = proc(req: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} - - HttpProcessCallback* {.deprecated.} = - proc(req: RequestFence): Future[HttpResponseRef] {.async.} + async: (raises: [CancelledError]).} HttpConnectionCallback* = proc(server: HttpServerRef, @@ -138,7 +136,7 @@ type headersTable: HttpTable body: seq[byte] flags: set[HttpResponseFlags] - state*: HttpResponseState + state*: HttpResponseState # TODO (cheatfate): Make this field private connection*: HttpConnectionRef streamType*: HttpResponseStreamType writer: AsyncStreamWriter @@ -163,14 +161,20 @@ type ByteChar* = string | seq[byte] proc init(htype: typedesc[HttpProcessError], error: HttpServerError, - exc: ref CatchableError, remote: Opt[TransportAddress], + exc: ref HttpError, remote: Opt[TransportAddress], code: HttpCode): HttpProcessError = HttpProcessError(kind: error, exc: exc, remote: remote, code: code) +proc init(htype: typedesc[HttpProcessError], error: HttpServerError, + remote: Opt[TransportAddress], code: HttpCode): HttpProcessError = + HttpProcessError(kind: error, remote: remote, code: code) + proc init(htype: typedesc[HttpProcessError], error: HttpServerError): HttpProcessError = HttpProcessError(kind: error) +proc defaultResponse*(exc: ref CatchableError): HttpResponseRef + proc new(htype: typedesc[HttpConnectionHolderRef], server: HttpServerRef, transp: StreamTransport, connectionId: string): HttpConnectionHolderRef = @@ -254,34 +258,22 @@ proc new*(htype: typedesc[HttpServerRef], maxHeadersSize: int = 8192, maxRequestBodySize: int = 1_048_576, dualstack = DualStackType.Auto): HttpResult[HttpServerRef] {. - deprecated: "raises missing from process callback".} = + deprecated: "Callback could raise only CancelledError, annotate with " & + "{.async: (raises: [CancelledError]).}".} = - proc processCallback2(req: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = - try: - await processCallback(req) - except CancelledError as exc: - raise exc - except HttpResponseError as exc: - raise exc - except CatchableError as exc: - # Emulate 3.x behavior - raise (ref HttpCriticalError)(msg: exc.msg, code: Http503) + proc wrap(req: RequestFence): Future[HttpResponseRef] {. + async: (raises: [CancelledError]).} = + try: + await processCallback(req) + except CancelledError as exc: + raise exc + except CatchableError as exc: + defaultResponse(exc) - HttpServerRef.new( - address = address, - processCallback = processCallback2, - serverFlags = serverFlags, - socketFlags = socketFlags, - serverUri = serverUri, - serverIdent = serverIdent, - maxConnections = maxConnections, - bufferSize = bufferSize, - backlogSize = backlogSize, - httpHeadersTimeout = httpHeadersTimeout, - maxHeadersSize = maxHeadersSize, - maxRequestBodySize = maxRequestBodySize, - dualstack = dualstack) + HttpServerRef.new(address, wrap, serverFlags, socketFlags, serverUri, + serverIdent, maxConnections, bufferSize, backlogSize, + httpHeadersTimeout, maxHeadersSize, maxRequestBodySize, + dualstack) proc getServerFlags(req: HttpRequestRef): set[HttpServerFlags] = var defaultFlags: set[HttpServerFlags] = {} @@ -304,6 +296,12 @@ proc getResponseFlags(req: HttpRequestRef): set[HttpResponseFlags] = else: defaultFlags +proc getResponseState*(response: HttpResponseRef): HttpResponseState = + response.state + +proc setResponseState(response: HttpResponseRef, state: HttpResponseState) = + response.state = state + proc getResponseVersion(reqFence: RequestFence): HttpVersion = if reqFence.isErr(): HttpVersion11 @@ -335,6 +333,18 @@ proc defaultResponse*(): HttpResponseRef = ## Create an empty response to return when request processor got no request. HttpResponseRef(state: HttpResponseState.Default, version: HttpVersion11) +proc defaultResponse*(exc: ref CatchableError): HttpResponseRef = + ## Create response with error code based on exception type. + if exc of AsyncTimeoutError: + HttpResponseRef(state: HttpResponseState.ErrorCode, status: Http408) + elif exc of HttpTransportError: + HttpResponseRef(state: HttpResponseState.Failed) + elif exc of HttpProtocolError: + let code = cast[ref HttpProtocolError](exc).code + HttpResponseRef(state: HttpResponseState.ErrorCode, status: code) + else: + HttpResponseRef(state: HttpResponseState.ErrorCode, status: Http503) + proc dumbResponse*(): HttpResponseRef {. deprecated: "Please use defaultResponse() instead".} = ## Create an empty response to return when request processor got no request. @@ -353,11 +363,11 @@ proc hasBody*(request: HttpRequestRef): bool = HttpRequestFlags.UnboundBody} != {} proc prepareRequest(conn: HttpConnectionRef, - req: HttpRequestHeader): HttpResultCode[HttpRequestRef] = + req: HttpRequestHeader): HttpResultMessage[HttpRequestRef] = var request = HttpRequestRef(connection: conn, state: HttpState.Alive) if req.version notin {HttpVersion10, HttpVersion11}: - return err(Http505) + return err(HttpMessage.init(Http505, "Unsupported HTTP protocol version")) request.scheme = if HttpServerFlags.Secure in conn.server.flags: @@ -372,14 +382,14 @@ proc prepareRequest(conn: HttpConnectionRef, block: let res = req.uri() if len(res) == 0: - return err(Http400) + return err(HttpMessage.init(Http400, "Invalid request URI")) res request.uri = if request.rawPath != "*": let uri = parseUri(request.rawPath) if uri.scheme notin ["http", "https", ""]: - return err(Http400) + return err(HttpMessage.init(Http400, "Unsupported URI scheme")) uri else: var uri = initUri() @@ -407,59 +417,61 @@ proc prepareRequest(conn: HttpConnectionRef, # Validating HTTP request headers # Some of the headers must be present only once. if table.count(ContentTypeHeader) > 1: - return err(Http400) + return err(HttpMessage.init(Http400, "Multiple Content-Type headers")) if table.count(ContentLengthHeader) > 1: - return err(Http400) + return err(HttpMessage.init(Http400, "Multiple Content-Length headers")) if table.count(TransferEncodingHeader) > 1: - return err(Http400) + return err(HttpMessage.init(Http400, + "Multuple Transfer-Encoding headers")) table # Preprocessing "Content-Encoding" header. request.contentEncoding = - block: - let res = getContentEncoding( - request.headers.getList(ContentEncodingHeader)) - if res.isErr(): - return err(Http400) - else: - res.get() + getContentEncoding( + request.headers.getList(ContentEncodingHeader)).valueOr: + let msg = "Incorrect or unsupported Content-Encoding header value" + return err(HttpMessage.init(Http400, msg)) # Preprocessing "Transfer-Encoding" header. request.transferEncoding = - block: - let res = getTransferEncoding( - request.headers.getList(TransferEncodingHeader)) - if res.isErr(): - return err(Http400) - else: - res.get() + getTransferEncoding( + request.headers.getList(TransferEncodingHeader)).valueOr: + let msg = "Incorrect or unsupported Transfer-Encoding header value" + return err(HttpMessage.init(Http400, msg)) # Almost all HTTP requests could have body (except TRACE), we perform some # steps to reveal information about body. - if ContentLengthHeader in request.headers: - let length = request.headers.getInt(ContentLengthHeader) - if length >= 0: - if request.meth == MethodTrace: - return err(Http400) - # Because of coversion to `int` we should avoid unexpected OverflowError. - if length > uint64(high(int)): - return err(Http413) - if length > uint64(conn.server.maxRequestBodySize): - return err(Http413) - request.contentLength = int(length) - request.requestFlags.incl(HttpRequestFlags.BoundBody) - else: - if TransferEncodingFlags.Chunked in request.transferEncoding: - if request.meth == MethodTrace: - return err(Http400) - request.requestFlags.incl(HttpRequestFlags.UnboundBody) + request.contentLength = + if ContentLengthHeader in request.headers: + let length = request.headers.getInt(ContentLengthHeader) + if length != 0: + if request.meth == MethodTrace: + let msg = "TRACE requests could not have request body" + return err(HttpMessage.init(Http400, msg)) + # Because of coversion to `int` we should avoid unexpected OverflowError. + if length > uint64(high(int)): + return err(HttpMessage.init(Http413, "Unsupported content length")) + if length > uint64(conn.server.maxRequestBodySize): + return err(HttpMessage.init(Http413, "Content length exceeds limits")) + request.requestFlags.incl(HttpRequestFlags.BoundBody) + int(length) + else: + 0 + else: + if TransferEncodingFlags.Chunked in request.transferEncoding: + if request.meth == MethodTrace: + let msg = "TRACE requests could not have request body" + return err(HttpMessage.init(Http400, msg)) + request.requestFlags.incl(HttpRequestFlags.UnboundBody) + 0 if request.hasBody(): # If request has body, we going to understand how its encoded. if ContentTypeHeader in request.headers: let contentType = getContentType(request.headers.getList(ContentTypeHeader)).valueOr: - return err(Http415) + let msg = "Incorrect or missing Content-Type header" + return err(HttpMessage.init(Http415, msg)) if contentType == UrlEncodedContentType: request.requestFlags.incl(HttpRequestFlags.UrlencodedForm) elif contentType == MultipartContentType: @@ -486,16 +498,17 @@ proc getBodyReader*(request: HttpRequestRef): HttpResult[HttpBodyReader] = uint64(request.contentLength)) ok(newHttpBodyReader(bstream)) elif HttpRequestFlags.UnboundBody in request.requestFlags: - let maxBodySize = request.connection.server.maxRequestBodySize - let cstream = newChunkedStreamReader(request.connection.reader) - let bstream = newBoundedStreamReader(cstream, uint64(maxBodySize), - comparison = BoundCmp.LessOrEqual) + let + maxBodySize = request.connection.server.maxRequestBodySize + cstream = newChunkedStreamReader(request.connection.reader) + bstream = newBoundedStreamReader(cstream, uint64(maxBodySize), + comparison = BoundCmp.LessOrEqual) ok(newHttpBodyReader(bstream, cstream)) else: err("Request do not have body available") proc handleExpect*(request: HttpRequestRef) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Handle expectation for ``Expect`` header. ## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect if HttpServerFlags.NoExpectHandler notin request.connection.server.flags: @@ -504,85 +517,50 @@ proc handleExpect*(request: HttpRequestRef) {. try: let message = $request.version & " " & $Http100 & "\r\n\r\n" await request.connection.writer.write(message) - except CancelledError as exc: - raise exc except AsyncStreamError as exc: - raiseHttpCriticalError( + raiseHttpWriteError( "Unable to send `100-continue` response, reason: " & $exc.msg) proc getBody*(request: HttpRequestRef): Future[seq[byte]] {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, + HttpTransportError, HttpProtocolError]).} = ## Obtain request's body as sequence of bytes. - let bodyReader = request.getBodyReader() - if bodyReader.isErr(): + let reader = request.getBodyReader().valueOr: return @[] - else: - var reader = bodyReader.get() - try: - await request.handleExpect() - let res = await reader.read() - if reader.hasOverflow(): - await reader.closeWait() - reader = nil - raiseHttpCriticalError(MaximumBodySizeError, Http413) - else: - await reader.closeWait() - reader = nil - return res - except CancelledError as exc: - if not(isNil(reader)): - await reader.closeWait() - raise exc - except HttpCriticalError as exc: - if not(isNil(reader)): - await reader.closeWait() - raise exc - except AsyncStreamError as exc: - let msg = "Unable to read request's body, reason: " & $exc.msg - if not(isNil(reader)): - await reader.closeWait() - raiseHttpCriticalError(msg) + try: + await request.handleExpect() + let res = await reader.read() + if reader.hasOverflow(): + raiseHttpRequestBodyTooLargeError() + res + except AsyncStreamError as exc: + let msg = "Unable to read request's body, reason: " & $exc.msg + raiseHttpReadError(msg) + finally: + await reader.closeWait() proc consumeBody*(request: HttpRequestRef): Future[void] {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpTransportError, + HttpProtocolError]).} = ## Consume/discard request's body. - let bodyReader = request.getBodyReader() - if bodyReader.isErr(): + let reader = request.getBodyReader().valueOr: return - else: - var reader = bodyReader.get() - try: - await request.handleExpect() - discard await reader.consume() - if reader.hasOverflow(): - await reader.closeWait() - reader = nil - raiseHttpCriticalError(MaximumBodySizeError, Http413) - else: - await reader.closeWait() - reader = nil - return - except CancelledError as exc: - if not(isNil(reader)): - await reader.closeWait() - raise exc - except HttpCriticalError as exc: - if not(isNil(reader)): - await reader.closeWait() - raise exc - except AsyncStreamError as exc: - let msg = "Unable to consume request's body, reason: " & $exc.msg - if not(isNil(reader)): - await reader.closeWait() - raiseHttpCriticalError(msg) + try: + await request.handleExpect() + discard await reader.consume() + if reader.hasOverflow(): raiseHttpRequestBodyTooLargeError() + except AsyncStreamError as exc: + let msg = "Unable to consume request's body, reason: " & $exc.msg + raiseHttpReadError(msg) + finally: + await reader.closeWait() proc getAcceptInfo*(request: HttpRequestRef): Result[AcceptInfo, cstring] = ## Returns value of `Accept` header as `AcceptInfo` object. ## ## If ``Accept`` header is missing in request headers, ``*/*`` content ## type will be returned. - let acceptHeader = request.headers.getString(AcceptHeaderName) - getAcceptInfo(acceptHeader) + getAcceptInfo(request.headers.getString(AcceptHeaderName)) proc preferredContentMediaType*(acceptHeader: string): MediaType = ## Returns preferred content-type using ``Accept`` header value specified by @@ -693,7 +671,7 @@ proc preferredContentType*(request: HttpRequestRef, proc sendErrorResponse(conn: HttpConnectionRef, version: HttpVersion, code: HttpCode, keepAlive = true, - datatype = "text/text", + datatype = "text/plain", databody = "") {. async: (raises: [CancelledError]).} = var answer = $version & " " & $code & "\r\n" @@ -721,39 +699,10 @@ proc sendErrorResponse(conn: HttpConnectionRef, version: HttpVersion, answer.add(databody) try: await conn.writer.write(answer) - except CancelledError as exc: - raise exc except AsyncStreamError: # We ignore errors here, because we indicating error already. discard -proc sendErrorResponse( - conn: HttpConnectionRef, - reqFence: RequestFence, - respError: HttpProcessError - ): Future[HttpProcessExitType] {.async: (raises: []).} = - let version = getResponseVersion(reqFence) - try: - if reqFence.isOk(): - case respError.kind - of HttpServerError.CriticalError: - await conn.sendErrorResponse(version, respError.code, false) - HttpProcessExitType.Graceful - of HttpServerError.RecoverableError: - await conn.sendErrorResponse(version, respError.code, true) - HttpProcessExitType.Graceful - of HttpServerError.CatchableError: - await conn.sendErrorResponse(version, respError.code, false) - HttpProcessExitType.Graceful - of HttpServerError.DisconnectError, - HttpServerError.InterruptError, - HttpServerError.TimeoutError: - raiseAssert("Unexpected response error: " & $respError.kind) - else: - HttpProcessExitType.Graceful - except CancelledError: - HttpProcessExitType.Immediate - proc sendDefaultResponse( conn: HttpConnectionRef, reqFence: RequestFence, @@ -794,6 +743,10 @@ proc sendDefaultResponse( await conn.sendErrorResponse(HttpVersion11, Http409, keepConnection.toBool()) keepConnection + of HttpResponseState.ErrorCode: + # Response with error code + await conn.sendErrorResponse(version, response.status, false) + HttpProcessExitType.Immediate of HttpResponseState.Sending, HttpResponseState.Failed, HttpResponseState.Cancelled: # Just drop connection, because we dont know at what stage we are @@ -810,27 +763,21 @@ proc sendDefaultResponse( of HttpServerError.TimeoutError: await conn.sendErrorResponse(version, reqFence.error.code, false) HttpProcessExitType.Graceful - of HttpServerError.CriticalError: - await conn.sendErrorResponse(version, reqFence.error.code, false) - HttpProcessExitType.Graceful - of HttpServerError.RecoverableError: - await conn.sendErrorResponse(version, reqFence.error.code, false) - HttpProcessExitType.Graceful - of HttpServerError.CatchableError: + of HttpServerError.ProtocolError: await conn.sendErrorResponse(version, reqFence.error.code, false) HttpProcessExitType.Graceful of HttpServerError.DisconnectError: # When `HttpServerFlags.NotifyDisconnect` is set. HttpProcessExitType.Immediate of HttpServerError.InterruptError: + # InterruptError should be handled earlier raiseAssert("Unexpected request error: " & $reqFence.error.kind) except CancelledError: HttpProcessExitType.Immediate - except CatchableError: - HttpProcessExitType.Immediate proc getRequest(conn: HttpConnectionRef): Future[HttpRequestRef] {. - async: (raises: [CancelledError, HttpError]).} = + async: (raises: [CancelledError, HttpDisconnectError, + HttpProtocolError]).} = try: conn.buffer.setLen(conn.server.maxHeadersSize) let res = await conn.reader.readUntil(addr conn.buffer[0], len(conn.buffer), @@ -838,15 +785,11 @@ proc getRequest(conn: HttpConnectionRef): Future[HttpRequestRef] {. conn.buffer.setLen(res) let header = parseRequest(conn.buffer) if header.failed(): - raiseHttpCriticalError("Malformed request recieved") - else: - let res = prepareRequest(conn, header) - if res.isErr(): - raiseHttpCriticalError("Invalid request received", res.error) - else: - return res.get() + raiseHttpProtocolError(Http400, "Malformed request recieved") + prepareRequest(conn, header).valueOr: + raiseHttpProtocolError(error) except AsyncStreamLimitError: - raiseHttpCriticalError("Maximum size of request headers reached", Http431) + raiseHttpProtocolError(Http431, "Maximum size of request headers reached") except AsyncStreamError: raiseHttpDisconnectError() @@ -915,7 +858,7 @@ proc createConnection(server: HttpServerRef, HttpConnectionRef.new(server, transp) proc `keepalive=`*(resp: HttpResponseRef, value: bool) = - doAssert(resp.state == HttpResponseState.Empty) + doAssert(resp.getResponseState() == HttpResponseState.Empty) if value: resp.flags.incl(HttpResponseFlags.KeepAlive) else: @@ -935,55 +878,6 @@ proc getRemoteAddress(connection: HttpConnectionRef): Opt[TransportAddress] = if isNil(connection): return Opt.none(TransportAddress) getRemoteAddress(connection.transp) -proc getResponseFence*(connection: HttpConnectionRef, - reqFence: RequestFence): Future[ResponseFence] {. - async: (raises: []).} = - try: - let res = await connection.server.processCallback(reqFence) - ResponseFence.ok(res) - except CancelledError: - ResponseFence.err(HttpProcessError.init( - HttpServerError.InterruptError)) - except HttpCriticalError as exc: - let address = connection.getRemoteAddress() - ResponseFence.err(HttpProcessError.init( - HttpServerError.CriticalError, exc, address, exc.code)) - except HttpRecoverableError as exc: - let address = connection.getRemoteAddress() - ResponseFence.err(HttpProcessError.init( - HttpServerError.RecoverableError, exc, address, exc.code)) - except HttpResponseError as exc: - # There should be only 2 children of HttpResponseError, and all of them - # should be handled. - raiseAssert "Unexpected response error " & $exc.name & ", reason: " & - $exc.msg - -proc getResponseFence*(server: HttpServerRef, - connFence: ConnectionFence): Future[ResponseFence] {. - async: (raises: []).} = - doAssert(connFence.isErr()) - try: - let - reqFence = RequestFence.err(connFence.error) - res = await server.processCallback(reqFence) - ResponseFence.ok(res) - except CancelledError: - ResponseFence.err(HttpProcessError.init( - HttpServerError.InterruptError)) - except HttpCriticalError as exc: - let address = Opt.none(TransportAddress) - ResponseFence.err(HttpProcessError.init( - HttpServerError.CriticalError, exc, address, exc.code)) - except HttpRecoverableError as exc: - let address = Opt.none(TransportAddress) - ResponseFence.err(HttpProcessError.init( - HttpServerError.RecoverableError, exc, address, exc.code)) - except HttpResponseError as exc: - # There should be only 2 children of HttpResponseError, and all of them - # should be handled. - raiseAssert "Unexpected response error " & $exc.name & ", reason: " & - $exc.msg - proc getRequestFence*(server: HttpServerRef, connection: HttpConnectionRef): Future[RequestFence] {. async: (raises: []).} = @@ -996,27 +890,21 @@ proc getRequestFence*(server: HttpServerRef, connection.currentRawQuery = Opt.some(res.rawPath) RequestFence.ok(res) except CancelledError: - RequestFence.err(HttpProcessError.init(HttpServerError.InterruptError)) - except AsyncTimeoutError as exc: + RequestFence.err( + HttpProcessError.init(HttpServerError.InterruptError)) + except AsyncTimeoutError: let address = connection.getRemoteAddress() - RequestFence.err(HttpProcessError.init( - HttpServerError.TimeoutError, exc, address, Http408)) - except HttpRecoverableError as exc: + RequestFence.err( + HttpProcessError.init(HttpServerError.TimeoutError, address, Http408)) + except HttpProtocolError as exc: let address = connection.getRemoteAddress() - RequestFence.err(HttpProcessError.init( - HttpServerError.RecoverableError, exc, address, exc.code)) - except HttpCriticalError as exc: + RequestFence.err( + HttpProcessError.init(HttpServerError.ProtocolError, exc, address, + exc.code)) + except HttpDisconnectError: let address = connection.getRemoteAddress() - RequestFence.err(HttpProcessError.init( - HttpServerError.CriticalError, exc, address, exc.code)) - except HttpDisconnectError as exc: - let address = connection.getRemoteAddress() - RequestFence.err(HttpProcessError.init( - HttpServerError.DisconnectError, exc, address, Http400)) - except CatchableError as exc: - let address = connection.getRemoteAddress() - RequestFence.err(HttpProcessError.init( - HttpServerError.CatchableError, exc, address, Http500)) + RequestFence.err( + HttpProcessError.init(HttpServerError.DisconnectError, address, Http400)) proc getConnectionFence*(server: HttpServerRef, transp: StreamTransport): Future[ConnectionFence] {. @@ -1026,16 +914,11 @@ proc getConnectionFence*(server: HttpServerRef, ConnectionFence.ok(res) except CancelledError: ConnectionFence.err(HttpProcessError.init(HttpServerError.InterruptError)) - except HttpCriticalError as exc: + except HttpConnectionError as exc: # On error `transp` will be closed by `createConnCallback()` call. let address = Opt.none(TransportAddress) ConnectionFence.err(HttpProcessError.init( - HttpServerError.CriticalError, exc, address, exc.code)) - except CatchableError as exc: - # On error `transp` will be closed by `createConnCallback()` call. - let address = Opt.none(TransportAddress) - ConnectionFence.err(HttpProcessError.init( - HttpServerError.CriticalError, exc, address, Http503)) + HttpServerError.DisconnectError, exc, address, Http400)) proc processRequest(server: HttpServerRef, connection: HttpConnectionRef, @@ -1045,30 +928,28 @@ proc processRequest(server: HttpServerRef, if requestFence.isErr(): case requestFence.error.kind of HttpServerError.InterruptError: + # Cancelled, exiting return HttpProcessExitType.Immediate of HttpServerError.DisconnectError: + # Remote peer disconnected if HttpServerFlags.NotifyDisconnect notin server.flags: return HttpProcessExitType.Immediate else: + # Request is incorrect or unsupported, sending notification discard - let responseFence = await getResponseFence(connection, requestFence) - if responseFence.isErr() and - (responseFence.error.kind == HttpServerError.InterruptError): + try: + let response = + try: + await connection.server.processCallback(requestFence) + except CancelledError: + # Cancelled, exiting + return HttpProcessExitType.Immediate + + await connection.sendDefaultResponse(requestFence, response) + finally: if requestFence.isOk(): await requestFence.get().closeWait() - return HttpProcessExitType.Immediate - - let res = - if responseFence.isErr(): - await connection.sendErrorResponse(requestFence, responseFence.error) - else: - await connection.sendDefaultResponse(requestFence, responseFence.get()) - - if requestFence.isOk(): - await requestFence.get().closeWait() - - res proc processLoop(holder: HttpConnectionHolderRef) {.async: (raises: []).} = let @@ -1077,10 +958,11 @@ proc processLoop(holder: HttpConnectionHolderRef) {.async: (raises: []).} = connectionId = holder.connectionId connection = block: - let res = await server.getConnectionFence(transp) + let res = await getConnectionFence(server, transp) if res.isErr(): if res.error.kind != HttpServerError.InterruptError: - discard await server.getResponseFence(res) + discard await noCancel( + server.processCallback(RequestFence.err(res.error))) server.connections.del(connectionId) return res.get() @@ -1089,13 +971,7 @@ proc processLoop(holder: HttpConnectionHolderRef) {.async: (raises: []).} = var runLoop = HttpProcessExitType.KeepAlive while runLoop == HttpProcessExitType.KeepAlive: - runLoop = - try: - await server.processRequest(connection, connectionId) - except CancelledError: - HttpProcessExitType.Immediate - except CatchableError as exc: - raiseAssert "Unexpected error [" & $exc.name & "] happens: " & $exc.msg + runLoop = await server.processRequest(connection, connectionId) case runLoop of HttpProcessExitType.KeepAlive: @@ -1104,7 +980,6 @@ proc processLoop(holder: HttpConnectionHolderRef) {.async: (raises: []).} = await connection.closeWait() of HttpProcessExitType.Graceful: await connection.gracefulCloseWait() - server.connections.del(connectionId) proc acceptClientLoop(server: HttpServerRef) {.async: (raises: []).} = @@ -1210,89 +1085,84 @@ proc getMultipartReader*(req: HttpRequestRef): HttpResult[MultiPartReaderRef] = err("Request's method do not supports multipart") proc post*(req: HttpRequestRef): Future[HttpTable] {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpTransportError, + HttpProtocolError]).} = ## Return POST parameters if req.postTable.isSome(): return req.postTable.get() - else: - if req.meth notin PostMethods: - return HttpTable.init() - if UrlencodedForm in req.requestFlags: - let queryFlags = - if QueryCommaSeparatedArray in req.connection.server.flags: - {QueryParamsFlag.CommaSeparatedArray} - else: - {} - var table = HttpTable.init() - # getBody() will handle `Expect`. - var body = await req.getBody() - # TODO (cheatfate) double copy here, because of `byte` to `char` - # conversion. - var strbody = newString(len(body)) - if len(body) > 0: - copyMem(addr strbody[0], addr body[0], len(body)) - for key, value in queryParams(strbody, queryFlags): - table.add(key, value) - req.postTable = Opt.some(table) - return table - elif MultipartForm in req.requestFlags: - var table = HttpTable.init() - let res = getMultipartReader(req) - if res.isErr(): - raiseHttpCriticalError("Unable to retrieve multipart form data") - var mpreader = res.get() + if req.meth notin PostMethods: + return HttpTable.init() - # We must handle `Expect` first. + if UrlencodedForm in req.requestFlags: + let queryFlags = + if QueryCommaSeparatedArray in req.connection.server.flags: + {QueryParamsFlag.CommaSeparatedArray} + else: + {} + var table = HttpTable.init() + # getBody() will handle `Expect`. + var body = await req.getBody() + # TODO (cheatfate) double copy here, because of `byte` to `char` + # conversion. + var strbody = newString(len(body)) + if len(body) > 0: + copyMem(addr strbody[0], addr body[0], len(body)) + for key, value in queryParams(strbody, queryFlags): + table.add(key, value) + req.postTable = Opt.some(table) + return table + elif MultipartForm in req.requestFlags: + var table = HttpTable.init() + let mpreader = getMultipartReader(req).valueOr: + raiseHttpProtocolError(Http400, + "Unable to retrieve multipart form data, reason: " & $error) + # Reading multipart/form-data parts. + var runLoop = true + while runLoop: + var part: MultiPart try: - await req.handleExpect() - except CancelledError as exc: - await mpreader.closeWait() - raise exc - except HttpCriticalError as exc: - await mpreader.closeWait() - raise exc + part = await mpreader.readPart() + var value = await part.getBody() - # Reading multipart/form-data parts. - var runLoop = true - while runLoop: - var part: MultiPart - try: - part = await mpreader.readPart() - var value = await part.getBody() - # TODO (cheatfate) double copy here, because of `byte` to `char` - # conversion. - var strvalue = newString(len(value)) - if len(value) > 0: - copyMem(addr strvalue[0], addr value[0], len(value)) - table.add(part.name, strvalue) + # TODO (cheatfate) double copy here, because of `byte` to `char` + # conversion. + var strvalue = newString(len(value)) + if len(value) > 0: + copyMem(addr strvalue[0], addr value[0], len(value)) + table.add(part.name, strvalue) + await part.closeWait() + except MultipartEOMError: + runLoop = false + except HttpWriteError as exc: + if not(part.isEmpty()): await part.closeWait() - except MultipartEOMError: - runLoop = false - except HttpCriticalError as exc: - if not(part.isEmpty()): - await part.closeWait() - await mpreader.closeWait() - raise exc - except CancelledError as exc: - if not(part.isEmpty()): - await part.closeWait() - await mpreader.closeWait() - raise exc - await mpreader.closeWait() - req.postTable = Opt.some(table) - return table - else: - if HttpRequestFlags.BoundBody in req.requestFlags: - if req.contentLength != 0: - raiseHttpCriticalError("Unsupported request body") - return HttpTable.init() - elif HttpRequestFlags.UnboundBody in req.requestFlags: - raiseHttpCriticalError("Unsupported request body") + await mpreader.closeWait() + raise exc + except HttpProtocolError as exc: + if not(part.isEmpty()): + await part.closeWait() + await mpreader.closeWait() + raise exc + except CancelledError as exc: + if not(part.isEmpty()): + await part.closeWait() + await mpreader.closeWait() + raise exc + await mpreader.closeWait() + req.postTable = Opt.some(table) + return table + else: + if HttpRequestFlags.BoundBody in req.requestFlags: + if req.contentLength != 0: + raiseHttpProtocolError(Http400, "Unsupported request body") + return HttpTable.init() + elif HttpRequestFlags.UnboundBody in req.requestFlags: + raiseHttpProtocolError(Http400, "Unsupported request body") proc setHeader*(resp: HttpResponseRef, key, value: string) = ## Sets value of header ``key`` to ``value``. - doAssert(resp.state == HttpResponseState.Empty) + doAssert(resp.getResponseState() == HttpResponseState.Empty) resp.headersTable.set(key, value) proc setHeaderDefault*(resp: HttpResponseRef, key, value: string) = @@ -1302,7 +1172,7 @@ proc setHeaderDefault*(resp: HttpResponseRef, key, value: string) = proc addHeader*(resp: HttpResponseRef, key, value: string) = ## Adds value ``value`` to header's ``key`` value. - doAssert(resp.state == HttpResponseState.Empty) + doAssert(resp.getResponseState() == HttpResponseState.Empty) resp.headersTable.add(key, value) proc getHeader*(resp: HttpResponseRef, key: string, @@ -1316,8 +1186,22 @@ proc hasHeader*(resp: HttpResponseRef, key: string): bool = key in resp.headersTable template checkPending(t: untyped) = - if t.state != HttpResponseState.Empty: - raiseHttpCriticalError("Response body was already sent") + let currentState = t.getResponseState() + doAssert(currentState == HttpResponseState.Empty, + "Response body was already sent [" & $currentState & "]") + +template checkStreamResponse(t: untyped) = + doAssert(HttpResponseFlags.Stream in t.flags, + "Response was not prepared") + +template checkStreamResponseState(t: untyped) = + doAssert(t.getResponseState() in + {HttpResponseState.Prepared, HttpResponseState.Sending}, + "Response is in the wrong state") + +template checkPointerLength(t1, t2: untyped) = + doAssert(not(isNil(t1)), "pbytes must not be nil") + doAssert(t2 >= 0, "nbytes should be bigger or equal to zero") func createHeaders(resp: HttpResponseRef): string = var answer = $(resp.version) & " " & $(resp.status) & "\r\n" @@ -1386,69 +1270,68 @@ proc preparePlainHeaders(resp: HttpResponseRef): string = resp.createHeaders() proc sendBody*(resp: HttpResponseRef, pbytes: pointer, nbytes: int) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Send HTTP response at once by using bytes pointer ``pbytes`` and length ## ``nbytes``. - doAssert(not(isNil(pbytes)), "pbytes must not be nil") - doAssert(nbytes >= 0, "nbytes should be bigger or equal to zero") + checkPointerLength(pbytes, nbytes) checkPending(resp) let responseHeaders = resp.prepareLengthHeaders(nbytes) - resp.state = HttpResponseState.Prepared + resp.setResponseState(HttpResponseState.Prepared) try: - resp.state = HttpResponseState.Sending + resp.setResponseState(HttpResponseState.Sending) await resp.connection.writer.write(responseHeaders) if nbytes > 0: await resp.connection.writer.write(pbytes, nbytes) - resp.state = HttpResponseState.Finished + resp.setResponseState(HttpResponseState.Finished) except CancelledError as exc: - resp.state = HttpResponseState.Cancelled + resp.setResponseState(HttpResponseState.Cancelled) raise exc except AsyncStreamError as exc: - resp.state = HttpResponseState.Failed - raiseHttpCriticalError("Unable to send response, reason: " & $exc.msg) + resp.setResponseState(HttpResponseState.Failed) + raiseHttpWriteError("Unable to send response, reason: " & $exc.msg) proc sendBody*(resp: HttpResponseRef, data: ByteChar) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Send HTTP response at once by using data ``data``. checkPending(resp) let responseHeaders = resp.prepareLengthHeaders(len(data)) - resp.state = HttpResponseState.Prepared + resp.setResponseState(HttpResponseState.Prepared) try: - resp.state = HttpResponseState.Sending + resp.setResponseState(HttpResponseState.Sending) await resp.connection.writer.write(responseHeaders) if len(data) > 0: await resp.connection.writer.write(data) - resp.state = HttpResponseState.Finished + resp.setResponseState(HttpResponseState.Finished) except CancelledError as exc: - resp.state = HttpResponseState.Cancelled + resp.setResponseState(HttpResponseState.Cancelled) raise exc except AsyncStreamError as exc: - resp.state = HttpResponseState.Failed - raiseHttpCriticalError("Unable to send response, reason: " & $exc.msg) + resp.setResponseState(HttpResponseState.Failed) + raiseHttpWriteError("Unable to send response, reason: " & $exc.msg) proc sendError*(resp: HttpResponseRef, code: HttpCode, body = "") {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Send HTTP error status response. checkPending(resp) resp.status = code let responseHeaders = resp.prepareLengthHeaders(len(body)) - resp.state = HttpResponseState.Prepared + resp.setResponseState(HttpResponseState.Prepared) try: - resp.state = HttpResponseState.Sending + resp.setResponseState(HttpResponseState.Sending) await resp.connection.writer.write(responseHeaders) if len(body) > 0: await resp.connection.writer.write(body) - resp.state = HttpResponseState.Finished + resp.setResponseState(HttpResponseState.Finished) except CancelledError as exc: - resp.state = HttpResponseState.Cancelled + resp.setResponseState(HttpResponseState.Cancelled) raise exc except AsyncStreamError as exc: - resp.state = HttpResponseState.Failed - raiseHttpCriticalError("Unable to send response, reason: " & $exc.msg) + resp.setResponseState(HttpResponseState.Failed) + raiseHttpWriteError("Unable to send response, reason: " & $exc.msg) proc prepare*(resp: HttpResponseRef, streamType = HttpResponseStreamType.Chunked) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Prepare for HTTP stream response. ## ## Such responses will be sent chunk by chunk using ``chunked`` encoding. @@ -1462,9 +1345,9 @@ proc prepare*(resp: HttpResponseRef, of HttpResponseStreamType.Chunked: resp.prepareChunkedHeaders() resp.streamType = streamType - resp.state = HttpResponseState.Prepared + resp.setResponseState(HttpResponseState.Prepared) try: - resp.state = HttpResponseState.Sending + resp.setResponseState(HttpResponseState.Sending) await resp.connection.writer.write(responseHeaders) case streamType of HttpResponseStreamType.Plain, HttpResponseStreamType.SSE: @@ -1473,117 +1356,105 @@ proc prepare*(resp: HttpResponseRef, resp.writer = newChunkedStreamWriter(resp.connection.writer) resp.flags.incl(HttpResponseFlags.Stream) except CancelledError as exc: - resp.state = HttpResponseState.Cancelled + resp.setResponseState(HttpResponseState.Cancelled) raise exc except AsyncStreamError as exc: - resp.state = HttpResponseState.Failed - raiseHttpCriticalError("Unable to send response, reason: " & $exc.msg) + resp.setResponseState(HttpResponseState.Failed) + raiseHttpWriteError("Unable to send response, reason: " & $exc.msg) proc prepareChunked*(resp: HttpResponseRef): Future[void] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Prepare for HTTP chunked stream response. ## ## Such responses will be sent chunk by chunk using ``chunked`` encoding. resp.prepare(HttpResponseStreamType.Chunked) proc preparePlain*(resp: HttpResponseRef): Future[void] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Prepare for HTTP plain stream response. ## ## Such responses will be sent without any encoding. resp.prepare(HttpResponseStreamType.Plain) proc prepareSSE*(resp: HttpResponseRef): Future[void] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Prepare for HTTP server-side event stream response. resp.prepare(HttpResponseStreamType.SSE) proc send*(resp: HttpResponseRef, pbytes: pointer, nbytes: int) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Send single chunk of data pointed by ``pbytes`` and ``nbytes``. - doAssert(not(isNil(pbytes)), "pbytes must not be nil") - doAssert(nbytes >= 0, "nbytes should be bigger or equal to zero") - if HttpResponseFlags.Stream notin resp.flags: - raiseHttpCriticalError("Response was not prepared") - if resp.state notin {HttpResponseState.Prepared, HttpResponseState.Sending}: - raiseHttpCriticalError("Response in incorrect state") + checkPointerLength(pbytes, nbytes) + resp.checkStreamResponse() + resp.checkStreamResponseState() try: - resp.state = HttpResponseState.Sending + resp.setResponseState(HttpResponseState.Sending) await resp.writer.write(pbytes, nbytes) - resp.state = HttpResponseState.Sending except CancelledError as exc: - resp.state = HttpResponseState.Cancelled + resp.setResponseState(HttpResponseState.Cancelled) raise exc except AsyncStreamError as exc: - resp.state = HttpResponseState.Failed - raiseHttpCriticalError("Unable to send response, reason: " & $exc.msg) + resp.setResponseState(HttpResponseState.Failed) + raiseHttpWriteError("Unable to send response, reason: " & $exc.msg) proc send*(resp: HttpResponseRef, data: ByteChar) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Send single chunk of data ``data``. - if HttpResponseFlags.Stream notin resp.flags: - raiseHttpCriticalError("Response was not prepared") - if resp.state notin {HttpResponseState.Prepared, HttpResponseState.Sending}: - raiseHttpCriticalError("Response in incorrect state") + resp.checkStreamResponse() + resp.checkStreamResponseState() try: - resp.state = HttpResponseState.Sending + resp.setResponseState(HttpResponseState.Sending) await resp.writer.write(data) - resp.state = HttpResponseState.Sending except CancelledError as exc: - resp.state = HttpResponseState.Cancelled + resp.setResponseState(HttpResponseState.Cancelled) raise exc except AsyncStreamError as exc: - resp.state = HttpResponseState.Failed - raiseHttpCriticalError("Unable to send response, reason: " & $exc.msg) + resp.setResponseState(HttpResponseState.Failed) + raiseHttpWriteError("Unable to send response, reason: " & $exc.msg) proc sendChunk*(resp: HttpResponseRef, pbytes: pointer, nbytes: int): Future[void] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = resp.send(pbytes, nbytes) proc sendChunk*(resp: HttpResponseRef, data: ByteChar): Future[void] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = resp.send(data) proc sendEvent*(resp: HttpResponseRef, eventName: string, data: string): Future[void] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Send server-side event with name ``eventName`` and payload ``data`` to ## remote peer. - let data = - block: - var res = "" - if len(eventName) > 0: - res.add("event: ") - res.add(eventName) - res.add("\r\n") - res.add("data: ") - res.add(data) - res.add("\r\n\r\n") - res - resp.send(data) + var res = "" + if len(eventName) > 0: + res.add("event: ") + res.add(eventName) + res.add("\r\n") + res.add("data: ") + res.add(data) + res.add("\r\n\r\n") + resp.send(res) proc finish*(resp: HttpResponseRef) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Sending last chunk of data, so it will indicate end of HTTP response. - if HttpResponseFlags.Stream notin resp.flags: - raiseHttpCriticalError("Response was not prepared") - if resp.state notin {HttpResponseState.Prepared, HttpResponseState.Sending}: - raiseHttpCriticalError("Response in incorrect state") + resp.checkStreamResponse() + resp.checkStreamResponseState() try: - resp.state = HttpResponseState.Sending + resp.setResponseState(HttpResponseState.Sending) await resp.writer.finish() - resp.state = HttpResponseState.Finished + resp.setResponseState(HttpResponseState.Finished) except CancelledError as exc: - resp.state = HttpResponseState.Cancelled + resp.setResponseState(HttpResponseState.Cancelled) raise exc except AsyncStreamError as exc: - resp.state = HttpResponseState.Failed - raiseHttpCriticalError("Unable to send response, reason: " & $exc.msg) + resp.setResponseState(HttpResponseState.Failed) + raiseHttpWriteError("Unable to send response, reason: " & $exc.msg) proc respond*(req: HttpRequestRef, code: HttpCode, content: ByteChar, headers: HttpTable): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Responds to the request with the specified ``HttpCode``, HTTP ``headers`` ## and ``content``. let response = req.getResponse() @@ -1595,18 +1466,18 @@ proc respond*(req: HttpRequestRef, code: HttpCode, content: ByteChar, proc respond*(req: HttpRequestRef, code: HttpCode, content: ByteChar): Future[HttpResponseRef] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Responds to the request with specified ``HttpCode`` and ``content``. respond(req, code, content, HttpTable.init()) proc respond*(req: HttpRequestRef, code: HttpCode): Future[HttpResponseRef] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Responds to the request with specified ``HttpCode`` only. respond(req, code, "", HttpTable.init()) proc redirect*(req: HttpRequestRef, code: HttpCode, location: string, headers: HttpTable): Future[HttpResponseRef] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Responds to the request with redirection to location ``location`` and ## additional headers ``headers``. ## @@ -1618,7 +1489,7 @@ proc redirect*(req: HttpRequestRef, code: HttpCode, proc redirect*(req: HttpRequestRef, code: HttpCode, location: Uri, headers: HttpTable): Future[HttpResponseRef] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Responds to the request with redirection to location ``location`` and ## additional headers ``headers``. ## @@ -1628,13 +1499,13 @@ proc redirect*(req: HttpRequestRef, code: HttpCode, proc redirect*(req: HttpRequestRef, code: HttpCode, location: Uri): Future[HttpResponseRef] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Responds to the request with redirection to location ``location``. redirect(req, code, location, HttpTable.init()) proc redirect*(req: HttpRequestRef, code: HttpCode, location: string): Future[HttpResponseRef] {. - async: (raw: true, raises: [CancelledError, HttpCriticalError]).} = + async: (raw: true, raises: [CancelledError, HttpWriteError]).} = ## Responds to the request with redirection to location ``location``. redirect(req, code, location, HttpTable.init()) diff --git a/chronos/apps/http/multipart.nim b/chronos/apps/http/multipart.nim index 83a4b56..302d6ef 100644 --- a/chronos/apps/http/multipart.nim +++ b/chronos/apps/http/multipart.nim @@ -18,7 +18,8 @@ import "."/[httptable, httpcommon, httpbodyrw] export asyncloop, httptable, httpcommon, httpbodyrw, asyncstream, httputils const - UnableToReadMultipartBody = "Unable to read multipart message body" + UnableToReadMultipartBody = "Unable to read multipart message body, reason: " + UnableToSendMultipartMessage = "Unable to send multipart message, reason: " type MultiPartSource* {.pure.} = enum @@ -69,7 +70,7 @@ type name*: string filename*: string - MultipartError* = object of HttpCriticalError + MultipartError* = object of HttpProtocolError MultipartEOMError* = object of MultipartError BChar* = byte | char @@ -105,7 +106,7 @@ func setPartNames(part: var MultiPart): HttpResult[void] = return err("Content-Disposition header value is incorrect") let dtype = disp.dispositionType(header.toOpenArrayByte(0, len(header) - 1)) if dtype.toLowerAscii() != "form-data": - return err("Content-Disposition type is incorrect") + return err("Content-Disposition header type is incorrect") for k, v in disp.fields(header.toOpenArrayByte(0, len(header) - 1)): case k.toLowerAscii() of "name": @@ -171,8 +172,17 @@ proc new*[B: BChar](mpt: typedesc[MultiPartReaderRef], stream: stream, offset: 0, boundary: fboundary, buffer: newSeq[byte](partHeadersMaxSize)) +template handleAsyncStreamReaderError(targ, excarg: untyped) = + if targ.hasOverflow(): + raiseHttpRequestBodyTooLargeError() + raiseHttpReadError(UnableToReadMultipartBody & $excarg.msg) + +template handleAsyncStreamWriterError(targ, excarg: untyped) = + targ.state = MultiPartWriterState.MessageFailure + raiseHttpWriteError(UnableToSendMultipartMessage & $excarg.msg) + proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpReadError, HttpProtocolError]).} = doAssert(mpr.kind == MultiPartSource.Stream) if mpr.firstTime: try: @@ -181,14 +191,11 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {. mpr.firstTime = false if not(startsWith(mpr.buffer.toOpenArray(0, len(mpr.boundary) - 3), mpr.boundary.toOpenArray(2, len(mpr.boundary) - 1))): - raiseHttpCriticalError("Unexpected boundary encountered") + raiseHttpProtocolError(Http400, "Unexpected boundary encountered") except CancelledError as exc: raise exc - except AsyncStreamError: - if mpr.stream.hasOverflow(): - raiseHttpCriticalError(MaximumBodySizeError, Http413) - else: - raiseHttpCriticalError(UnableToReadMultipartBody) + except AsyncStreamError as exc: + handleAsyncStreamReaderError(mpr.stream, exc) # Reading part's headers try: @@ -202,9 +209,9 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {. raise newException(MultipartEOMError, "End of multipart message") else: - raiseHttpCriticalError("Incorrect multipart header found") + raiseHttpProtocolError(Http400, "Incorrect multipart header found") if mpr.buffer[0] != 0x0D'u8 or mpr.buffer[1] != 0x0A'u8: - raiseHttpCriticalError("Incorrect multipart boundary found") + raiseHttpProtocolError(Http400, "Incorrect multipart boundary found") # If two bytes are CRLF we are at the part beginning. # Reading part's headers @@ -212,7 +219,7 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {. HeadersMark) var headersList = parseHeaders(mpr.buffer.toOpenArray(0, res - 1), false) if headersList.failed(): - raiseHttpCriticalError("Incorrect multipart's headers found") + raiseHttpProtocolError(Http400, "Incorrect multipart's headers found") inc(mpr.counter) var part = MultiPart( @@ -228,45 +235,35 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {. let sres = part.setPartNames() if sres.isErr(): - raiseHttpCriticalError($sres.error) + raiseHttpProtocolError(Http400, $sres.error) return part except CancelledError as exc: raise exc - except AsyncStreamError: - if mpr.stream.hasOverflow(): - raiseHttpCriticalError(MaximumBodySizeError, Http413) - else: - raiseHttpCriticalError(UnableToReadMultipartBody) + except AsyncStreamError as exc: + handleAsyncStreamReaderError(mpr.stream, exc) proc getBody*(mp: MultiPart): Future[seq[byte]] {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpReadError, HttpProtocolError]).} = ## Get multipart's ``mp`` value as sequence of bytes. case mp.kind of MultiPartSource.Stream: try: - let res = await mp.stream.read() - return res - except AsyncStreamError: - if mp.breader.hasOverflow(): - raiseHttpCriticalError(MaximumBodySizeError, Http413) - else: - raiseHttpCriticalError(UnableToReadMultipartBody) + await mp.stream.read() + except AsyncStreamError as exc: + handleAsyncStreamReaderError(mp.breader, exc) of MultiPartSource.Buffer: - return mp.buffer + mp.buffer proc consumeBody*(mp: MultiPart) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpReadError, HttpProtocolError]).} = ## Discard multipart's ``mp`` value. case mp.kind of MultiPartSource.Stream: try: discard await mp.stream.consume() - except AsyncStreamError: - if mp.breader.hasOverflow(): - raiseHttpCriticalError(MaximumBodySizeError, Http413) - else: - raiseHttpCriticalError(UnableToReadMultipartBody) + except AsyncStreamError as exc: + handleAsyncStreamReaderError(mp.breader, exc) of MultiPartSource.Buffer: discard @@ -533,7 +530,7 @@ proc new*[B: BChar](mpt: typedesc[MultiPartWriterRef], proc prepareHeaders(partMark: openArray[byte], name: string, filename: string, headers: HttpTable): string = - const ContentDisposition = "Content-Disposition" + const ContentDispositionHeader = "Content-Disposition" let qname = block: let res = quoteCheck(name) @@ -546,10 +543,10 @@ proc prepareHeaders(partMark: openArray[byte], name: string, filename: string, res.get() var buffer = newString(len(partMark)) copyMem(addr buffer[0], unsafeAddr partMark[0], len(partMark)) - buffer.add(ContentDisposition) + buffer.add(ContentDispositionHeader) buffer.add(": ") - if ContentDisposition in headers: - buffer.add(headers.getString(ContentDisposition)) + if ContentDispositionHeader in headers: + buffer.add(headers.getString(ContentDispositionHeader)) buffer.add("\r\n") else: buffer.add("form-data; name=\"") @@ -562,7 +559,7 @@ proc prepareHeaders(partMark: openArray[byte], name: string, filename: string, buffer.add("\r\n") for k, v in headers.stringItems(): - if k != toLowerAscii(ContentDisposition): + if k != ContentDispositionHeader: if len(v) > 0: buffer.add(k) buffer.add(": ") @@ -572,7 +569,7 @@ proc prepareHeaders(partMark: openArray[byte], name: string, filename: string, buffer proc begin*(mpw: MultiPartWriterRef) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Starts multipart message form and write approprate markers to output ## stream. doAssert(mpw.kind == MultiPartSource.Stream) @@ -580,10 +577,9 @@ proc begin*(mpw: MultiPartWriterRef) {. # write "--" try: await mpw.stream.write(mpw.beginMark) - except AsyncStreamError: - mpw.state = MultiPartWriterState.MessageFailure - raiseHttpCriticalError("Unable to start multipart message") - mpw.state = MultiPartWriterState.MessageStarted + mpw.state = MultiPartWriterState.MessageStarted + except AsyncStreamError as exc: + handleAsyncStreamWriterError(mpw, exc) proc begin*(mpw: var MultiPartWriter) = ## Starts multipart message form and write approprate markers to output @@ -596,7 +592,7 @@ proc begin*(mpw: var MultiPartWriter) = proc beginPart*(mpw: MultiPartWriterRef, name: string, filename: string, headers: HttpTable) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Starts part of multipart message and write appropriate ``headers`` to the ## output stream. ## @@ -611,9 +607,8 @@ proc beginPart*(mpw: MultiPartWriterRef, name: string, try: await mpw.stream.write(buffer) mpw.state = MultiPartWriterState.PartStarted - except AsyncStreamError: - mpw.state = MultiPartWriterState.MessageFailure - raiseHttpCriticalError("Unable to start multipart part") + except AsyncStreamError as exc: + handleAsyncStreamWriterError(mpw, exc) proc beginPart*(mpw: var MultiPartWriter, name: string, filename: string, headers: HttpTable) = @@ -632,7 +627,7 @@ proc beginPart*(mpw: var MultiPartWriter, name: string, mpw.state = MultiPartWriterState.PartStarted proc write*(mpw: MultiPartWriterRef, pbytes: pointer, nbytes: int) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Write part's data ``data`` to the output stream. doAssert(mpw.kind == MultiPartSource.Stream) doAssert(mpw.state == MultiPartWriterState.PartStarted) @@ -640,12 +635,10 @@ proc write*(mpw: MultiPartWriterRef, pbytes: pointer, nbytes: int) {. # write of data await mpw.stream.write(pbytes, nbytes) except AsyncStreamError as exc: - mpw.state = MultiPartWriterState.MessageFailure - raiseHttpCriticalError( - "Unable to write multipart data, reason: " & $exc.msg) + handleAsyncStreamWriterError(mpw, exc) proc write*(mpw: MultiPartWriterRef, data: seq[byte]) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Write part's data ``data`` to the output stream. doAssert(mpw.kind == MultiPartSource.Stream) doAssert(mpw.state == MultiPartWriterState.PartStarted) @@ -653,12 +646,10 @@ proc write*(mpw: MultiPartWriterRef, data: seq[byte]) {. # write of data await mpw.stream.write(data) except AsyncStreamError as exc: - mpw.state = MultiPartWriterState.MessageFailure - raiseHttpCriticalError( - "Unable to write multipart data, reason: " & $exc.msg) + handleAsyncStreamWriterError(mpw, exc) proc write*(mpw: MultiPartWriterRef, data: string) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Write part's data ``data`` to the output stream. doAssert(mpw.kind == MultiPartSource.Stream) doAssert(mpw.state == MultiPartWriterState.PartStarted) @@ -666,9 +657,7 @@ proc write*(mpw: MultiPartWriterRef, data: string) {. # write of data await mpw.stream.write(data) except AsyncStreamError as exc: - mpw.state = MultiPartWriterState.MessageFailure - raiseHttpCriticalError( - "Unable to write multipart data, reason: " & $exc.msg) + handleAsyncStreamWriterError(mpw, exc) proc write*(mpw: var MultiPartWriter, pbytes: pointer, nbytes: int) = ## Write part's data ``data`` to the output stream. @@ -692,7 +681,7 @@ proc write*(mpw: var MultiPartWriter, data: openArray[char]) = mpw.buffer.add(data.toOpenArrayByte(0, len(data) - 1)) proc finishPart*(mpw: MultiPartWriterRef) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Finish multipart's message part and send proper markers to output stream. doAssert(mpw.state == MultiPartWriterState.PartStarted) try: @@ -700,9 +689,7 @@ proc finishPart*(mpw: MultiPartWriterRef) {. await mpw.stream.write(mpw.finishPartMark) mpw.state = MultiPartWriterState.PartFinished except AsyncStreamError as exc: - mpw.state = MultiPartWriterState.MessageFailure - raiseHttpCriticalError( - "Unable to finish multipart message part, reason: " & $exc.msg) + handleAsyncStreamWriterError(mpw, exc) proc finishPart*(mpw: var MultiPartWriter) = ## Finish multipart's message part and send proper markers to output stream. @@ -713,7 +700,7 @@ proc finishPart*(mpw: var MultiPartWriter) = mpw.state = MultiPartWriterState.PartFinished proc finish*(mpw: MultiPartWriterRef) {. - async: (raises: [CancelledError, HttpCriticalError]).} = + async: (raises: [CancelledError, HttpWriteError]).} = ## Finish multipart's message form and send finishing markers to the output ## stream. doAssert(mpw.kind == MultiPartSource.Stream) @@ -723,9 +710,7 @@ proc finish*(mpw: MultiPartWriterRef) {. await mpw.stream.write(mpw.finishMark) mpw.state = MultiPartWriterState.MessageFinished except AsyncStreamError as exc: - mpw.state = MultiPartWriterState.MessageFailure - raiseHttpCriticalError( - "Unable to finish multipart message, reason: " & $exc.msg) + handleAsyncStreamWriterError(mpw, exc) proc finish*(mpw: var MultiPartWriter): seq[byte] = ## Finish multipart's message form and send finishing markers to the output diff --git a/chronos/apps/http/shttpserver.nim b/chronos/apps/http/shttpserver.nim index f7e377f..6272bb2 100644 --- a/chronos/apps/http/shttpserver.nim +++ b/chronos/apps/http/shttpserver.nim @@ -164,22 +164,21 @@ proc new*(htype: typedesc[SecureHttpServerRef], maxRequestBodySize: int = 1_048_576, dualstack = DualStackType.Auto ): HttpResult[SecureHttpServerRef] {. - deprecated: "raises missing from process callback".} = - proc processCallback2(req: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = - try: - await processCallback(req) - except CancelledError as exc: - raise exc - except HttpResponseError as exc: - raise exc - except CatchableError as exc: - # Emulate 3.x behavior - raise (ref HttpCriticalError)(msg: exc.msg, code: Http503) + deprecated: "Callback could raise only CancelledError, annotate with " & + "{.async: (raises: [CancelledError]).}".} = + + proc wrap(req: RequestFence): Future[HttpResponseRef] {. + async: (raises: [CancelledError]).} = + try: + await processCallback(req) + except CancelledError as exc: + raise exc + except CatchableError as exc: + defaultResponse(exc) SecureHttpServerRef.new( address = address, - processCallback = processCallback2, + processCallback = wrap, tlsPrivateKey = tlsPrivateKey, tlsCertificate = tlsCertificate, serverFlags = serverFlags, @@ -194,4 +193,4 @@ proc new*(htype: typedesc[SecureHttpServerRef], maxHeadersSize = maxHeadersSize, maxRequestBodySize = maxRequestBodySize, dualstack = dualstack - ) \ No newline at end of file + ) diff --git a/tests/testhttpclient.nim b/tests/testhttpclient.nim index d2a355d..967f896 100644 --- a/tests/testhttpclient.nim +++ b/tests/testhttpclient.nim @@ -85,7 +85,8 @@ suite "HTTP client testing suite": res proc createServer(address: TransportAddress, - process: HttpProcessCallback2, secure: bool): HttpServerRef = + process: HttpProcessCallback2, + secure: bool): HttpServerRef = let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} serverFlags = {HttpServerFlags.Http11Pipeline} @@ -128,18 +129,24 @@ suite "HTTP client testing suite": (MethodPatch, "/test/patch") ] proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() case request.uri.path of "/test/get", "/test/post", "/test/head", "/test/put", "/test/delete", "/test/trace", "/test/options", "/test/connect", "/test/patch", "/test/error": - return await request.respond(Http200, request.uri.path) + try: + await request.respond(Http200, request.uri.path) + except HttpWriteError as exc: + defaultResponse(exc) else: - return await request.respond(Http404, "Page not found") + try: + await request.respond(Http404, "Page not found") + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -195,7 +202,7 @@ suite "HTTP client testing suite": "LONGCHUNKRESPONSE") ] proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() case request.uri.path @@ -203,46 +210,58 @@ suite "HTTP client testing suite": var response = request.getResponse() var data = createBigMessage(ResponseTests[0][4], ResponseTests[0][2]) response.status = Http200 - await response.sendBody(data) - return response + try: + await response.sendBody(data) + except HttpWriteError as exc: + return defaultResponse(exc) + response of "/test/long_size_response": var response = request.getResponse() var data = createBigMessage(ResponseTests[1][4], ResponseTests[1][2]) response.status = Http200 - await response.sendBody(data) - return response + try: + await response.sendBody(data) + except HttpWriteError as exc: + return defaultResponse(exc) + response of "/test/short_chunked_response": var response = request.getResponse() var data = createBigMessage(ResponseTests[2][4], ResponseTests[2][2]) response.status = Http200 - await response.prepare() - var offset = 0 - while true: - if len(data) == offset: - break - let toWrite = min(1024, len(data) - offset) - await response.sendChunk(addr data[offset], toWrite) - offset = offset + toWrite - await response.finish() - return response + try: + await response.prepare() + var offset = 0 + while true: + if len(data) == offset: + break + let toWrite = min(1024, len(data) - offset) + await response.sendChunk(addr data[offset], toWrite) + offset = offset + toWrite + await response.finish() + except HttpWriteError as exc: + return defaultResponse(exc) + response of "/test/long_chunked_response": var response = request.getResponse() var data = createBigMessage(ResponseTests[3][4], ResponseTests[3][2]) response.status = Http200 - await response.prepare() - var offset = 0 - while true: - if len(data) == offset: - break - let toWrite = min(1024, len(data) - offset) - await response.sendChunk(addr data[offset], toWrite) - offset = offset + toWrite - await response.finish() - return response + try: + await response.prepare() + var offset = 0 + while true: + if len(data) == offset: + break + let toWrite = min(1024, len(data) - offset) + await response.sendChunk(addr data[offset], toWrite) + offset = offset + toWrite + await response.finish() + except HttpWriteError as exc: + return defaultResponse(exc) + response else: - return await request.respond(Http404, "Page not found") + defaultResponse() else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -311,21 +330,26 @@ suite "HTTP client testing suite": (MethodPost, "/test/big_request", 262400) ] proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() case request.uri.path of "/test/big_request": - if request.hasBody(): - let body = await request.getBody() - let digest = $secureHash(string.fromBytes(body)) - return await request.respond(Http200, digest) - else: - return await request.respond(Http400, "Missing content body") + try: + if request.hasBody(): + let body = await request.getBody() + let digest = $secureHash(string.fromBytes(body)) + await request.respond(Http200, digest) + else: + await request.respond(Http400, "Missing content body") + except HttpProtocolError as exc: + defaultResponse(exc) + except HttpTransportError as exc: + defaultResponse(exc) else: - return await request.respond(Http404, "Page not found") + defaultResponse() else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -381,21 +405,27 @@ suite "HTTP client testing suite": (MethodPost, "/test/big_chunk_request", 262400) ] proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() case request.uri.path of "/test/big_chunk_request": - if request.hasBody(): - let body = await request.getBody() - let digest = $secureHash(string.fromBytes(body)) - return await request.respond(Http200, digest) - else: - return await request.respond(Http400, "Missing content body") + try: + if request.hasBody(): + let + body = await request.getBody() + digest = $secureHash(string.fromBytes(body)) + await request.respond(Http200, digest) + else: + await request.respond(Http400, "Missing content body") + except HttpProtocolError as exc: + defaultResponse(exc) + except HttpTransportError as exc: + defaultResponse(exc) else: - return await request.respond(Http404, "Page not found") + defaultResponse() else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -455,23 +485,28 @@ suite "HTTP client testing suite": ] proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() case request.uri.path of "/test/post/urlencoded_size", "/test/post/urlencoded_chunked": - if request.hasBody(): - var postTable = await request.post() - let body = postTable.getString("field1") & ":" & - postTable.getString("field2") & ":" & - postTable.getString("field3") - return await request.respond(Http200, body) - else: - return await request.respond(Http400, "Missing content body") + try: + if request.hasBody(): + var postTable = await request.post() + let body = postTable.getString("field1") & ":" & + postTable.getString("field2") & ":" & + postTable.getString("field3") + await request.respond(Http200, body) + else: + await request.respond(Http400, "Missing content body") + except HttpTransportError as exc: + defaultResponse(exc) + except HttpProtocolError as exc: + defaultResponse(exc) else: - return await request.respond(Http404, "Page not found") + defaultResponse() else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -554,23 +589,28 @@ suite "HTTP client testing suite": ] proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() case request.uri.path of "/test/post/multipart_size", "/test/post/multipart_chunked": - if request.hasBody(): - var postTable = await request.post() - let body = postTable.getString("field1") & ":" & - postTable.getString("field2") & ":" & - postTable.getString("field3") - return await request.respond(Http200, body) - else: - return await request.respond(Http400, "Missing content body") + try: + if request.hasBody(): + var postTable = await request.post() + let body = postTable.getString("field1") & ":" & + postTable.getString("field2") & ":" & + postTable.getString("field3") + await request.respond(Http200, body) + else: + await request.respond(Http400, "Missing content body") + except HttpProtocolError as exc: + defaultResponse(exc) + except HttpTransportError as exc: + defaultResponse(exc) else: - return await request.respond(Http404, "Page not found") + defaultResponse() else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -649,26 +689,29 @@ suite "HTTP client testing suite": var lastAddress: Uri proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - case request.uri.path - of "/": - return await request.redirect(Http302, "/redirect/1") - of "/redirect/1": - return await request.redirect(Http302, "/next/redirect/2") - of "/next/redirect/2": - return await request.redirect(Http302, "redirect/3") - of "/next/redirect/redirect/3": - return await request.redirect(Http302, "next/redirect/4") - of "/next/redirect/redirect/next/redirect/4": - return await request.redirect(Http302, lastAddress) - of "/final/5": - return await request.respond(Http200, "ok-5") - else: - return await request.respond(Http404, "Page not found") + try: + case request.uri.path + of "/": + await request.redirect(Http302, "/redirect/1") + of "/redirect/1": + await request.redirect(Http302, "/next/redirect/2") + of "/next/redirect/2": + await request.redirect(Http302, "redirect/3") + of "/next/redirect/redirect/3": + await request.redirect(Http302, "next/redirect/4") + of "/next/redirect/redirect/next/redirect/4": + await request.redirect(Http302, lastAddress) + of "/final/5": + await request.respond(Http200, "ok-5") + else: + await request.respond(Http404, "Page not found") + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -706,8 +749,8 @@ suite "HTTP client testing suite": proc testSendCancelLeaksTest(secure: bool): Future[bool] {.async.} = proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = - return defaultResponse() + async: (raises: [CancelledError]).} = + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -756,8 +799,8 @@ suite "HTTP client testing suite": proc testOpenCancelLeaksTest(secure: bool): Future[bool] {.async.} = proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = - return defaultResponse() + async: (raises: [CancelledError]).} = + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() @@ -868,20 +911,23 @@ suite "HTTP client testing suite": (data2.status, data2.data.bytesToString(), count)] proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - case request.uri.path - of "/keep": - let headers = HttpTable.init([("connection", "keep-alive")]) - return await request.respond(Http200, "ok", headers = headers) - of "/drop": - let headers = HttpTable.init([("connection", "close")]) - return await request.respond(Http200, "ok", headers = headers) - else: - return await request.respond(Http404, "Page not found") + try: + case request.uri.path + of "/keep": + let headers = HttpTable.init([("connection", "keep-alive")]) + await request.respond(Http200, "ok", headers = headers) + of "/drop": + let headers = HttpTable.init([("connection", "close")]) + await request.respond(Http200, "ok", headers = headers) + else: + await request.respond(Http404, "Page not found") + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, false) server.start() @@ -1004,16 +1050,19 @@ suite "HTTP client testing suite": return (data.status, data.data.bytesToString(), 0) proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - case request.uri.path - of "/test": - return await request.respond(Http200, "ok") - else: - return await request.respond(Http404, "Page not found") + try: + case request.uri.path + of "/test": + await request.respond(Http200, "ok") + else: + await request.respond(Http404, "Page not found") + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, false) server.start() @@ -1064,19 +1113,22 @@ suite "HTTP client testing suite": return (data.status, data.data.bytesToString(), 0) proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - case request.uri.path - of "/test": - return await request.respond(Http200, "ok") - of "/keep-test": - let headers = HttpTable.init([("Connection", "keep-alive")]) - return await request.respond(Http200, "not-alive", headers) - else: - return await request.respond(Http404, "Page not found") + try: + case request.uri.path + of "/test": + await request.respond(Http200, "ok") + of "/keep-test": + let headers = HttpTable.init([("Connection", "keep-alive")]) + await request.respond(Http200, "not-alive", headers) + else: + await request.respond(Http404, "Page not found") + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, false) server.start() @@ -1180,58 +1232,61 @@ suite "HTTP client testing suite": true proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - if request.uri.path.startsWith("/test/single/"): - let index = - block: - var res = -1 - for index, value in SingleGoodTests.pairs(): - if value[0] == request.uri.path: - res = index - break - res - if index < 0: - return await request.respond(Http404, "Page not found") - var response = request.getResponse() - response.status = Http200 - await response.sendBody(SingleGoodTests[index][1]) - return response - elif request.uri.path.startsWith("/test/multiple/"): - let index = - block: - var res = -1 - for index, value in MultipleGoodTests.pairs(): - if value[0] == request.uri.path: - res = index - break - res - if index < 0: - return await request.respond(Http404, "Page not found") - var response = request.getResponse() - response.status = Http200 - await response.sendBody(MultipleGoodTests[index][1]) - return response - elif request.uri.path.startsWith("/test/overflow/"): - let index = - block: - var res = -1 - for index, value in OverflowTests.pairs(): - if value[0] == request.uri.path: - res = index - break - res - if index < 0: - return await request.respond(Http404, "Page not found") - var response = request.getResponse() - response.status = Http200 - await response.sendBody(OverflowTests[index][1]) - return response - else: - return await request.respond(Http404, "Page not found") + try: + if request.uri.path.startsWith("/test/single/"): + let index = + block: + var res = -1 + for index, value in SingleGoodTests.pairs(): + if value[0] == request.uri.path: + res = index + break + res + if index < 0: + return await request.respond(Http404, "Page not found") + var response = request.getResponse() + response.status = Http200 + await response.sendBody(SingleGoodTests[index][1]) + response + elif request.uri.path.startsWith("/test/multiple/"): + let index = + block: + var res = -1 + for index, value in MultipleGoodTests.pairs(): + if value[0] == request.uri.path: + res = index + break + res + if index < 0: + return await request.respond(Http404, "Page not found") + var response = request.getResponse() + response.status = Http200 + await response.sendBody(MultipleGoodTests[index][1]) + response + elif request.uri.path.startsWith("/test/overflow/"): + let index = + block: + var res = -1 + for index, value in OverflowTests.pairs(): + if value[0] == request.uri.path: + res = index + break + res + if index < 0: + return await request.respond(Http404, "Page not found") + var response = request.getResponse() + response.status = Http200 + await response.sendBody(OverflowTests[index][1]) + response + else: + defaultResponse() + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() var server = createServer(initTAddress("127.0.0.1:0"), process, secure) server.start() diff --git a/tests/testhttpserver.nim b/tests/testhttpserver.nim index 33d5ea1..0183f1b 100644 --- a/tests/testhttpserver.nim +++ b/tests/testhttpserver.nim @@ -64,7 +64,7 @@ suite "HTTP server testing suite": proc testTooBigBodyChunked(operation: TooBigTest): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() try: @@ -77,13 +77,15 @@ suite "HTTP server testing suite": let ptable {.used.} = await request.post() of PostMultipartTest: let ptable {.used.} = await request.post() - except HttpCriticalError as exc: + defaultResponse() + except HttpTransportError as exc: + defaultResponse(exc) + except HttpProtocolError as exc: if exc.code == Http413: serverRes = true - # Reraising exception, because processor should properly handle it. - raise exc + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -128,14 +130,17 @@ suite "HTTP server testing suite": proc testTimeout(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - return await request.respond(Http200, "TEST_OK", HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK", HttpTable.init()) + except HttpWriteError as exc: + defaultResponse(exc) else: if r.error.kind == HttpServerError.TimeoutError: serverRes = true - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), @@ -158,14 +163,17 @@ suite "HTTP server testing suite": proc testEmpty(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - return await request.respond(Http200, "TEST_OK", HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK", HttpTable.init()) + except HttpWriteError as exc: + defaultResponse(exc) else: - if r.error.kind == HttpServerError.CriticalError: + if r.error.kind == HttpServerError.ProtocolError: serverRes = true - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), @@ -188,14 +196,17 @@ suite "HTTP server testing suite": proc testTooBig(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - return await request.respond(Http200, "TEST_OK", HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK", HttpTable.init()) + except HttpWriteError as exc: + defaultResponse(exc) else: - if r.error.error == HttpServerError.CriticalError: + if r.error.error == HttpServerError.ProtocolError: serverRes = true - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -219,13 +230,11 @@ suite "HTTP server testing suite": proc testTooBigBody(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = - if r.isOk(): - discard - else: - if r.error.error == HttpServerError.CriticalError: + async: (raises: [CancelledError]).} = + if r.isErr(): + if r.error.error == HttpServerError.ProtocolError: serverRes = true - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -266,7 +275,7 @@ suite "HTTP server testing suite": proc testQuery(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() var kres = newSeq[string]() @@ -274,11 +283,14 @@ suite "HTTP server testing suite": kres.add(k & ":" & v) sort(kres) serverRes = true - return await request.respond(Http200, "TEST_OK:" & kres.join(":"), - HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + except HttpWriteError as exc: + serverRes = false + defaultResponse(exc) else: - serverRes = false - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -296,10 +308,9 @@ suite "HTTP server testing suite": "GET /?a=%D0%9F&%D0%A4=%D0%91&b=%D0%A6&c=%D0%AE HTTP/1.0\r\n\r\n") await server.stop() await server.closeWait() - let r = serverRes and - (data1.find("TEST_OK:a:1:a:2:b:3:c:4") >= 0) and - (data2.find("TEST_OK:a:П:b:Ц:c:Ю:Ф:Б") >= 0) - return r + serverRes and + (data1.find("TEST_OK:a:1:a:2:b:3:c:4") >= 0) and + (data2.find("TEST_OK:a:П:b:Ц:c:Ю:Ф:Б") >= 0) check waitFor(testQuery()) == true @@ -307,7 +318,7 @@ suite "HTTP server testing suite": proc testHeaders(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() var kres = newSeq[string]() @@ -315,11 +326,14 @@ suite "HTTP server testing suite": kres.add(k & ":" & v) sort(kres) serverRes = true - return await request.respond(Http200, "TEST_OK:" & kres.join(":"), - HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + except HttpWriteError as exc: + serverRes = false + defaultResponse(exc) else: - serverRes = false - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -351,21 +365,30 @@ suite "HTTP server testing suite": proc testPostUrl(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): var kres = newSeq[string]() let request = r.get() if request.meth in PostMethods: - let post = await request.post() + let post = + try: + await request.post() + except HttpProtocolError as exc: + return defaultResponse(exc) + except HttpTransportError as exc: + return defaultResponse(exc) for k, v in post.stringItems(): kres.add(k & ":" & v) sort(kres) - serverRes = true - return await request.respond(Http200, "TEST_OK:" & kres.join(":"), - HttpTable.init()) + serverRes = true + try: + await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + except HttpWriteError as exc: + serverRes = false + defaultResponse(exc) else: - serverRes = false - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -395,21 +418,30 @@ suite "HTTP server testing suite": proc testPostUrl2(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): var kres = newSeq[string]() let request = r.get() if request.meth in PostMethods: - let post = await request.post() + let post = + try: + await request.post() + except HttpProtocolError as exc: + return defaultResponse(exc) + except HttpTransportError as exc: + return defaultResponse(exc) for k, v in post.stringItems(): kres.add(k & ":" & v) sort(kres) - serverRes = true - return await request.respond(Http200, "TEST_OK:" & kres.join(":"), - HttpTable.init()) + serverRes = true + try: + await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + except HttpWriteError as exc: + serverRes = false + defaultResponse(exc) else: - serverRes = false - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -440,21 +472,30 @@ suite "HTTP server testing suite": proc testPostMultipart(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): var kres = newSeq[string]() let request = r.get() if request.meth in PostMethods: - let post = await request.post() + let post = + try: + await request.post() + except HttpProtocolError as exc: + return defaultResponse(exc) + except HttpTransportError as exc: + return defaultResponse(exc) for k, v in post.stringItems(): kres.add(k & ":" & v) sort(kres) - serverRes = true - return await request.respond(Http200, "TEST_OK:" & kres.join(":"), - HttpTable.init()) + serverRes = true + try: + await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + except HttpWriteError as exc: + serverRes = false + defaultResponse(exc) else: - serverRes = false - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -496,21 +537,31 @@ suite "HTTP server testing suite": proc testPostMultipart2(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): var kres = newSeq[string]() let request = r.get() if request.meth in PostMethods: - let post = await request.post() + let post = + try: + await request.post() + except HttpProtocolError as exc: + return defaultResponse(exc) + except HttpTransportError as exc: + return defaultResponse(exc) for k, v in post.stringItems(): kres.add(k & ":" & v) sort(kres) serverRes = true - return await request.respond(Http200, "TEST_OK:" & kres.join(":"), - HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + except HttpWriteError as exc: + serverRes = false + defaultResponse(exc) else: serverRes = false - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -566,16 +617,19 @@ suite "HTTP server testing suite": var count = 0 proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() inc(count) if count == ClientsCount: eventWait.fire() await eventContinue.wait() - return await request.respond(Http404, "", HttpTable.init()) + try: + await request.respond(Http404, "", HttpTable.init()) + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -1230,23 +1284,26 @@ suite "HTTP server testing suite": proc testPostMultipart2(): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() let response = request.getResponse() - await response.prepareSSE() - await response.send("event: event1\r\ndata: data1\r\n\r\n") - await response.send("event: event2\r\ndata: data2\r\n\r\n") - await response.sendEvent("event3", "data3") - await response.sendEvent("event4", "data4") - await response.send("data: data5\r\n\r\n") - await response.sendEvent("", "data6") - await response.finish() - serverRes = true - return response + try: + await response.prepareSSE() + await response.send("event: event1\r\ndata: data1\r\n\r\n") + await response.send("event: event2\r\ndata: data2\r\n\r\n") + await response.sendEvent("event3", "data3") + await response.sendEvent("event4", "data4") + await response.send("data: data5\r\n\r\n") + await response.sendEvent("", "data6") + await response.finish() + serverRes = true + response + except HttpWriteError as exc: + serverRes = false + defaultResponse(exc) else: - serverRes = false - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process, @@ -1306,12 +1363,15 @@ suite "HTTP server testing suite": ] proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - return await request.respond(Http200, "TEST_OK", HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK", HttpTable.init()) + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() for test in TestMessages: let @@ -1360,12 +1420,15 @@ suite "HTTP server testing suite": TestRequest = "GET /httpdebug HTTP/1.1\r\nConnection: keep-alive\r\n\r\n" proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - return await request.respond(Http200, "TEST_OK", HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK", HttpTable.init()) + except HttpWriteError as exc: + defaultResponse(exc) else: - return defaultResponse() + defaultResponse() proc client(address: TransportAddress, data: string): Future[StreamTransport] {.async.} = diff --git a/tests/testshttpserver.nim b/tests/testshttpserver.nim index 3ff2565..18e84a9 100644 --- a/tests/testshttpserver.nim +++ b/tests/testshttpserver.nim @@ -108,15 +108,18 @@ suite "Secure HTTP server testing suite": proc testHTTPS(address: TransportAddress): Future[bool] {.async.} = var serverRes = false proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() serverRes = true - return await request.respond(Http200, "TEST_OK:" & $request.meth, - HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK:" & $request.meth, + HttpTable.init()) + except HttpWriteError as exc: + serverRes = false + defaultResponse(exc) else: - serverRes = false - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let serverFlags = {Secure} @@ -146,16 +149,18 @@ suite "Secure HTTP server testing suite": var serverRes = false var testFut = newFuture[void]() proc process(r: RequestFence): Future[HttpResponseRef] {. - async: (raises: [CancelledError, HttpResponseError]).} = + async: (raises: [CancelledError]).} = if r.isOk(): let request = r.get() - serverRes = false - return await request.respond(Http200, "TEST_OK:" & $request.meth, - HttpTable.init()) + try: + await request.respond(Http200, "TEST_OK:" & $request.meth, + HttpTable.init()) + except HttpWriteError as exc: + defaultResponse(exc) else: serverRes = true testFut.complete() - return defaultResponse() + defaultResponse() let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} let serverFlags = {Secure}