diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift index 97d64b42..d7b8b69a 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import class Foundation.JSONEncoder import Logging import NIO import NIOHTTP1 @@ -68,14 +67,10 @@ extension Lambda { body = buffer case .failure(let error): url += Consts.postErrorURLSuffix - let error = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)") - switch error.toJson() { - case .failure(let jsonError): - return self.eventLoop.makeFailedFuture(RuntimeError.json(jsonError)) - case .success(let json): - body = self.allocator.buffer(capacity: json.utf8.count) - body!.writeString(json) - } + let errorResponse = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)") + let bytes = errorResponse.toJSONBytes() + body = self.allocator.buffer(capacity: bytes.count) + body!.writeBytes(bytes) } logger.debug("reporting results to lambda runtime engine using \(url)") return self.httpClient.post(url: url, body: body).flatMapThrowing { response in @@ -99,28 +94,23 @@ extension Lambda { func reportInitializationError(logger: Logger, error: Error) -> EventLoopFuture { let url = Consts.postInitErrorURL let errorResponse = ErrorResponse(errorType: Consts.initializationError, errorMessage: "\(error)") - var body: ByteBuffer - switch errorResponse.toJson() { - case .failure(let jsonError): - return self.eventLoop.makeFailedFuture(RuntimeError.json(jsonError)) - case .success(let json): - body = self.allocator.buffer(capacity: json.utf8.count) - body.writeString(json) - logger.warning("reporting initialization error to lambda runtime engine using \(url)") - return self.httpClient.post(url: url, body: body).flatMapThrowing { response in - guard response.status == .accepted else { - throw RuntimeError.badStatusCode(response.status) - } - return () - }.flatMapErrorThrowing { error in - switch error { - case HTTPClient.Errors.timeout: - throw RuntimeError.upstreamError("timeout") - case HTTPClient.Errors.connectionResetByPeer: - throw RuntimeError.upstreamError("connectionResetByPeer") - default: - throw error - } + let bytes = errorResponse.toJSONBytes() + var body = self.allocator.buffer(capacity: bytes.count) + body.writeBytes(bytes) + logger.warning("reporting initialization error to lambda runtime engine using \(url)") + return self.httpClient.post(url: url, body: body).flatMapThrowing { response in + guard response.status == .accepted else { + throw RuntimeError.badStatusCode(response.status) + } + return () + }.flatMapErrorThrowing { error in + switch error { + case HTTPClient.Errors.timeout: + throw RuntimeError.upstreamError("timeout") + case HTTPClient.Errors.connectionResetByPeer: + throw RuntimeError.upstreamError("connectionResetByPeer") + default: + throw error } } } @@ -142,15 +132,16 @@ internal struct ErrorResponse: Codable { var errorMessage: String } -private extension ErrorResponse { - func toJson() -> Result { - let encoder = JSONEncoder() - do { - let data = try encoder.encode(self) - return .success(String(data: data, encoding: .utf8) ?? "unknown error") - } catch { - return .failure(error) - } +internal extension ErrorResponse { + func toJSONBytes() -> [UInt8] { + var bytes = [UInt8]() + bytes.append(UInt8(ascii: "{")) + bytes.append(contentsOf: #""errorType":"# .utf8) + self.errorType.encodeAsJSONString(into: &bytes) + bytes.append(contentsOf: #","errorMessage":"# .utf8) + self.errorMessage.encodeAsJSONString(into: &bytes) + bytes.append(UInt8(ascii: "}")) + return bytes } } diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift index 929fea77..d31c7ca5 100644 --- a/Sources/AWSLambdaRuntime/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -78,3 +78,37 @@ internal extension DispatchWallTime { Int64(bitPattern: self.rawValue) / -1_000_000 } } + +extension String { + func encodeAsJSONString(into bytes: inout [UInt8]) { + bytes.append(UInt8(ascii: "\"")) + let stringBytes = self.utf8 + var startCopyIndex = stringBytes.startIndex + var nextIndex = startCopyIndex + + while nextIndex != stringBytes.endIndex { + switch stringBytes[nextIndex] { + case 0 ..< 32, UInt8(ascii: "\""), UInt8(ascii: "\\"): + // All Unicode characters may be placed within the + // quotation marks, except for the characters that MUST be escaped: + // quotation mark, reverse solidus, and the control characters (U+0000 + // through U+001F). + // https://tools.ietf.org/html/rfc7159#section-7 + + // copy the current range over + bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex]) + bytes.append(UInt8(ascii: "\\")) + bytes.append(stringBytes[nextIndex]) + + nextIndex = stringBytes.index(after: nextIndex) + startCopyIndex = nextIndex + default: + nextIndex = stringBytes.index(after: nextIndex) + } + } + + // copy everything, that hasn't been copied yet + bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex]) + bytes.append(UInt8(ascii: "\"")) + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift index 88a1f1d9..56bd5280 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift @@ -191,6 +191,24 @@ class LambdaRuntimeClientTest: XCTestCase { } } + func testErrorResponseToJSON() { + // we want to check if quotes and back slashes are correctly escaped + let windowsError = ErrorResponse( + errorType: "error", + errorMessage: #"underlyingError: "An error with a windows path C:\Windows\""# + ) + let windowsBytes = windowsError.toJSONBytes() + XCTAssertEqual(#"{"errorType":"error","errorMessage":"underlyingError: \"An error with a windows path C:\\Windows\\\""}"#, String(decoding: windowsBytes, as: Unicode.UTF8.self)) + + // we want to check if unicode sequences work + let emojiError = ErrorResponse( + errorType: "error", + errorMessage: #"๐Ÿฅ‘๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง"# + ) + let emojiBytes = emojiError.toJSONBytes() + XCTAssertEqual(#"{"errorType":"error","errorMessage":"๐Ÿฅ‘๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง"}"#, String(decoding: emojiBytes, as: Unicode.UTF8.self)) + } + class Behavior: LambdaServerBehavior { var state = 0