Skip to content

Remove use of JSONEncoder for Error Reporting #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 31 additions & 40 deletions Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//

import class Foundation.JSONEncoder
import Logging
import NIO
import NIOHTTP1
Expand Down Expand Up @@ -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
Expand All @@ -99,28 +94,23 @@ extension Lambda {
func reportInitializationError(logger: Logger, error: Error) -> EventLoopFuture<Void> {
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
}
}
}
Expand All @@ -142,15 +132,16 @@ internal struct ErrorResponse: Codable {
var errorMessage: String
}

private extension ErrorResponse {
func toJson() -> Result<String, Error> {
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
}
}

Expand Down
34 changes: 34 additions & 0 deletions Sources/AWSLambdaRuntime/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "\""))
}
}
18 changes: 18 additions & 0 deletions Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down