Skip to content

Commit

Permalink
swift: add conversion from Request to headers (#288)
Browse files Browse the repository at this point in the history
Adds converters to go from `Request` to a set of headers that upstream Envoy understands.

This will be utilized by the request logic in the `Envoy` class.

Previous consideration: envoyproxy/envoy-mobile#275

Signed-off-by: Michael Rebello <me@michaelrebello.com>
Signed-off-by: JP Simard <jp@jpsim.com>
  • Loading branch information
rebello95 authored and jpsim committed Nov 28, 2022
1 parent e73248b commit ecfe5ea
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 3 deletions.
1 change: 1 addition & 0 deletions mobile/library/swift/src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ swift_static_framework(
"LogLevel.swift",
"Request.swift",
"RequestBuilder.swift",
"RequestMapper.swift",
"RequestMethod.swift",
"ResponseHandler.swift",
"RetryPolicy.swift",
Expand Down
25 changes: 25 additions & 0 deletions mobile/library/swift/src/RequestMapper.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion mobile/library/swift/src/RetryPolicyMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions mobile/library/swift/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
93 changes: 93 additions & 0 deletions mobile/library/swift/test/RequestMapperTests.swift
Original file line number Diff line number Diff line change
@@ -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"])
}
}
4 changes: 2 additions & 2 deletions mobile/library/swift/test/RetryPolicyMapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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())
}
}

0 comments on commit ecfe5ea

Please sign in to comment.