diff --git a/mobile/library/swift/src/BUILD b/mobile/library/swift/src/BUILD index 762d113ff6aa..f31c3c8c4031 100644 --- a/mobile/library/swift/src/BUILD +++ b/mobile/library/swift/src/BUILD @@ -11,6 +11,7 @@ swift_static_framework( "LogLevel.swift", "Request.swift", "RequestBuilder.swift", + "RequestMapper.swift", "RequestMethod.swift", "ResponseHandler.swift", "RetryPolicy.swift", diff --git a/mobile/library/swift/src/RequestMapper.swift b/mobile/library/swift/src/RequestMapper.swift new file mode 100644 index 000000000000..c9e4be08cf08 --- /dev/null +++ b/mobile/library/swift/src/RequestMapper.swift @@ -0,0 +1,25 @@ +private let kRestrictedHeaderPrefix = ":" + +extension Request { + /// Returns a set of outbound headers that include HTTP + /// information on the URL, method, and additional headers. + /// + /// - returns: Outbound headers to send with an HTTP request. + func outboundHeaders() -> [String: String] { + var headers = self.headers + .filter { !$0.key.hasPrefix(kRestrictedHeaderPrefix) } + .mapValues { $0.joined(separator: ",") } + .reduce(into: [ + ":method": self.method.stringValue, + ":scheme": self.scheme, + ":authority": self.authority, + ":path": self.path, + ]) { $0[$1.key] = $1.value } + + if let retryPolicy = self.retryPolicy { + headers = headers.merging(retryPolicy.outboundHeaders()) { _, retryHeader in retryHeader } + } + + return headers + } +} diff --git a/mobile/library/swift/src/RetryPolicyMapper.swift b/mobile/library/swift/src/RetryPolicyMapper.swift index 68df23cd85e1..ccf538db75f9 100644 --- a/mobile/library/swift/src/RetryPolicyMapper.swift +++ b/mobile/library/swift/src/RetryPolicyMapper.swift @@ -2,7 +2,7 @@ extension RetryPolicy { /// Converts the retry policy to a set of headers recognized by Envoy. /// /// - returns: The header representation of the retry policy. - func toHeaders() -> [String: String] { + func outboundHeaders() -> [String: String] { var headers = [ "x-envoy-max-retries": "\(self.maxRetryCount)", "x-envoy-retry-on": self.retryOn diff --git a/mobile/library/swift/test/BUILD b/mobile/library/swift/test/BUILD index 57fa2123a89f..1fa1f37597b1 100644 --- a/mobile/library/swift/test/BUILD +++ b/mobile/library/swift/test/BUILD @@ -9,6 +9,13 @@ envoy_mobile_swift_test( ], ) +envoy_mobile_swift_test( + name = "request_mapper_tests", + srcs = [ + "RequestMapperTests.swift", + ], +) + envoy_mobile_swift_test( name = "retry_policy_tests", srcs = [ diff --git a/mobile/library/swift/test/RequestMapperTests.swift b/mobile/library/swift/test/RequestMapperTests.swift new file mode 100644 index 000000000000..52472ea14e76 --- /dev/null +++ b/mobile/library/swift/test/RequestMapperTests.swift @@ -0,0 +1,93 @@ +@testable import Envoy +import XCTest + +final class RequestMapperTests: XCTestCase { + func testAddsMethodToHeaders() { + let headers = RequestBuilder(method: .post, scheme: "https", authority: "x.y.z", path: "/foo") + .build() + .outboundHeaders() + XCTAssertEqual("POST", headers[":method"]) + } + + func testAddsSchemeToHeaders() { + let headers = RequestBuilder(method: .post, scheme: "https", authority: "x.y.z", path: "/foo") + .build() + .outboundHeaders() + XCTAssertEqual("https", headers[":scheme"]) + } + + func testAddsAuthorityToHeaders() { + let headers = RequestBuilder(method: .post, scheme: "https", authority: "x.y.z", path: "/foo") + .build() + .outboundHeaders() + XCTAssertEqual("x.y.z", headers[":authority"]) + } + + func testAddsPathToHeaders() { + let headers = RequestBuilder(method: .post, scheme: "https", authority: "x.y.z", path: "/foo") + .build() + .outboundHeaders() + XCTAssertEqual("/foo", headers[":path"]) + } + + func testJoinsHeaderValuesWithTheSameKey() { + let headers = RequestBuilder(method: .post, scheme: "https", authority: "x.y.z", path: "/foo") + .addHeader(name: "foo", value: "1") + .addHeader(name: "foo", value: "2") + .build() + .outboundHeaders() + XCTAssertEqual("1,2", headers["foo"]) + } + + func testStripsHeadersWithSemicolonPrefix() { + let headers = RequestBuilder(method: .post, scheme: "https", authority: "x.y.z", path: "/foo") + .addHeader(name: ":restricted", value: "someValue") + .build() + .outboundHeaders() + XCTAssertNil(headers[":restricted"]) + } + + func testCannotOverrideStandardRestrictedHeaders() { + let headers = RequestBuilder(method: .post, scheme: "https", authority: "x.y.z", path: "/foo") + .addHeader(name: ":scheme", value: "override") + .addHeader(name: ":authority", value: "override") + .addHeader(name: ":path", value: "override") + .build() + .outboundHeaders() + + XCTAssertEqual("https", headers[":scheme"]) + XCTAssertEqual("x.y.z", headers[":authority"]) + XCTAssertEqual("/foo", headers[":path"]) + } + + func testIncludesRetryPolicyHeaders() { + let retryPolicy = RetryPolicy(maxRetryCount: 123, retryOn: RetryRule.allCases, + perRetryTimeoutMS: 9001) + let retryHeaders = retryPolicy.outboundHeaders() + let requestHeaders = RequestBuilder(method: .post, scheme: "https", + authority: "x.y.z", path: "/foo") + .addHeader(name: "foo", value: "bar") + .addRetryPolicy(retryPolicy) + .build() + .outboundHeaders() + + XCTAssertEqual("bar", requestHeaders["foo"]) + XCTAssertFalse(retryHeaders.isEmpty) + for (retryHeader, expectedValue) in retryHeaders { + XCTAssertEqual(expectedValue, requestHeaders[retryHeader]) + } + } + + func testRetryPolicyTakesPrecedenceOverManuallySetRetryHeaders() { + let retryPolicy = RetryPolicy(maxRetryCount: 123, retryOn: RetryRule.allCases, + perRetryTimeoutMS: 9001) + let requestHeaders = RequestBuilder(method: .post, scheme: "https", + authority: "x.y.z", path: "/foo") + .addHeader(name: "x-envoy-max-retries", value: "override") + .addRetryPolicy(retryPolicy) + .build() + .outboundHeaders() + + XCTAssertEqual("123", requestHeaders["x-envoy-max-retries"]) + } +} diff --git a/mobile/library/swift/test/RetryPolicyMapperTests.swift b/mobile/library/swift/test/RetryPolicyMapperTests.swift index 73cc43ec1cef..1b1f435a1208 100644 --- a/mobile/library/swift/test/RetryPolicyMapperTests.swift +++ b/mobile/library/swift/test/RetryPolicyMapperTests.swift @@ -12,7 +12,7 @@ final class RetryPolicyMapperTests: XCTestCase { "x-envoy-upstream-rq-per-try-timeout-ms": "9001", ] - XCTAssertEqual(expectedHeaders, policy.toHeaders()) + XCTAssertEqual(expectedHeaders, policy.outboundHeaders()) } func testConvertingToHeadersWithoutRetryTimeoutExcludesPerRetryTimeoutHeader() { @@ -24,6 +24,6 @@ final class RetryPolicyMapperTests: XCTestCase { "x-envoy-retry-on": "5xx,gateway-error,connect-failure,retriable-4xx,refused-upstream", ] - XCTAssertEqual(expectedHeaders, policy.toHeaders()) + XCTAssertEqual(expectedHeaders, policy.outboundHeaders()) } }