Skip to content

Commit ec48f4f

Browse files
Convenience methods for socket paths (#235)
* Added additional tests for socketPath-based requests Motivation: While going through the existing tests, I identified a few more instances where we could add some testing. Modifications: Added one test that verifies Requests are being decoded correctly, and improved three others to check for path parsing, error throwing, and schema casing respectively. Result: Tests that continue to pass, but that will also catch any incompatible changes in the future. * Added some convenience initializers to URL and methods to Request for making requests to socket paths Motivation: Creating URLs for connecting to servers bound to socket paths currently requires some additional code to get exactly right. It would be nice to have convenience methods on both URL and Request to assist here. Modifications: - Refactored the get/post/patch/put/delete methods so they all call into a one line execute() method. - Added variations on the above methods so they can be called with socket paths (both over HTTP and HTTPS). - Added public convenience initializers to URL to support the above, and so socket path URLs can be easily created in other situations. - Added unit tests for creating socket path URLs, and testing the new suite of convenience execute methods (that, er, test `HTTPMETHOD`s). (patch, put, and delete are now also tested as a result of these tests) - Updated the read me with basic usage instructions. Result: New methods that allow for easily creating requests to socket paths, and passing tests to go with them. * Removed some of the new public methods added for creating a socket-path based request Motivation: I previously added too much new public API that will most likely not be necessary, and can be better accessed using a generic execute method. Modifications: Removed the get/post/patch/put/delete methods that were specific to socket paths. Result: Less new public API. * Renamed execute(url:) methods such that the HTTP method is the first argument in the parameter list Motivation: If these are intended to be general methods for building simple requests, then it makes sense to have the method be the first parameter in the list. Modifications: Moved the `method: HTTPMethod` parameter to the front of the list for all `execute([...] url: [...])` methods, and made it default to .GET. I also changed the url parameter to be `urlPath` for the two socketPath based execute methods. Result: A cleaner public interface for users of the API. * Fixed some minor issues introduces with logging Motivation: Some of the convenience request methods weren't properly adapted for logging. Modifications: - Removed a doc comment from patch() that incorrectly referenced a logger. - Fixed an issue where patch() would call into post(). - Added a doc comment to delete() that references the logger. - Tests for the above come in the next commit... Result: Correct documentation and functionality for the patch() and delete() methods. * Updated logging tests to also check the new execute methods Motivation: The logging tests previously didn't check for socket path-based requests. Modifications: Updated the `testAllMethodsLog()` and `testAllMethodsLog()` tests to include checks for each of the new `execute()` methods. Result: Two more tests that pass.
1 parent 8e60b94 commit ec48f4f

8 files changed

+421
-38
lines changed

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,22 @@ httpClient.execute(request: request, delegate: delegate).futureResult.whenSucces
157157
print(count)
158158
}
159159
```
160+
161+
### Unix Domain Socket Paths
162+
Connecting to servers bound to socket paths is easy:
163+
```swift
164+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
165+
httpClient.execute(.GET, socketPath: "/tmp/myServer.socket", urlPath: "/path/to/resource").whenComplete (...)
166+
```
167+
168+
Connecting over TLS to a unix domain socket path is possible as well:
169+
```swift
170+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
171+
httpClient.execute(.POST, secureSocketPath: "/tmp/myServer.socket", urlPath: "/path/to/resource", body: .string("hello")).whenComplete (...)
172+
```
173+
174+
Direct URLs can easily be contructed to be executed in other scenarios:
175+
```swift
176+
let socketPathBasedURL = URL(httpURLWithSocketPath: "/tmp/myServer.socket", uri: "/path/to/resource")
177+
let secureSocketPathBasedURL = URL(httpsURLWithSocketPath: "/tmp/myServer.socket", uri: "/path/to/resource")
178+
```

Sources/AsyncHTTPClient/HTTPClient.swift

+62-28
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,7 @@ public class HTTPClient {
237237
/// - deadline: Point in time by which the request must complete.
238238
/// - logger: The logger to use for this request.
239239
public func get(url: String, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
240-
do {
241-
let request = try Request(url: url, method: .GET)
242-
return self.execute(request: request, deadline: deadline, logger: logger)
243-
} catch {
244-
return self.eventLoopGroup.next().makeFailedFuture(error)
245-
}
240+
return self.execute(.GET, url: url, deadline: deadline, logger: logger)
246241
}
247242

248243
/// Execute `POST` request using specified URL.
@@ -263,12 +258,7 @@ public class HTTPClient {
263258
/// - deadline: Point in time by which the request must complete.
264259
/// - logger: The logger to use for this request.
265260
public func post(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
266-
do {
267-
let request = try HTTPClient.Request(url: url, method: .POST, body: body)
268-
return self.execute(request: request, deadline: deadline, logger: logger)
269-
} catch {
270-
return self.eventLoopGroup.next().makeFailedFuture(error)
271-
}
261+
return self.execute(.POST, url: url, body: body, deadline: deadline, logger: logger)
272262
}
273263

274264
/// Execute `PATCH` request using specified URL.
@@ -277,9 +267,8 @@ public class HTTPClient {
277267
/// - url: Remote URL.
278268
/// - body: Request body.
279269
/// - deadline: Point in time by which the request must complete.
280-
/// - logger: The logger to use for this request.
281270
public func patch(url: String, body: Body? = nil, deadline: NIODeadline? = nil) -> EventLoopFuture<Response> {
282-
return self.post(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled)
271+
return self.patch(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled)
283272
}
284273

285274
/// Execute `PATCH` request using specified URL.
@@ -290,12 +279,7 @@ public class HTTPClient {
290279
/// - deadline: Point in time by which the request must complete.
291280
/// - logger: The logger to use for this request.
292281
public func patch(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
293-
do {
294-
let request = try HTTPClient.Request(url: url, method: .PATCH, body: body)
295-
return self.execute(request: request, deadline: deadline, logger: logger)
296-
} catch {
297-
return self.eventLoopGroup.next().makeFailedFuture(error)
298-
}
282+
return self.execute(.PATCH, url: url, body: body, deadline: deadline, logger: logger)
299283
}
300284

301285
/// Execute `PUT` request using specified URL.
@@ -316,12 +300,7 @@ public class HTTPClient {
316300
/// - deadline: Point in time by which the request must complete.
317301
/// - logger: The logger to use for this request.
318302
public func put(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
319-
do {
320-
let request = try HTTPClient.Request(url: url, method: .PUT, body: body)
321-
return self.execute(request: request, deadline: deadline, logger: logger)
322-
} catch {
323-
return self.eventLoopGroup.next().makeFailedFuture(error)
324-
}
303+
return self.execute(.PUT, url: url, body: body, deadline: deadline, logger: logger)
325304
}
326305

327306
/// Execute `DELETE` request using specified URL.
@@ -338,10 +317,65 @@ public class HTTPClient {
338317
/// - parameters:
339318
/// - url: Remote URL.
340319
/// - deadline: The time when the request must have been completed by.
320+
/// - logger: The logger to use for this request.
341321
public func delete(url: String, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
322+
return self.execute(.DELETE, url: url, deadline: deadline, logger: logger)
323+
}
324+
325+
/// Execute arbitrary HTTP request using specified URL.
326+
///
327+
/// - parameters:
328+
/// - method: Request method.
329+
/// - url: Request url.
330+
/// - body: Request body.
331+
/// - deadline: Point in time by which the request must complete.
332+
/// - logger: The logger to use for this request.
333+
public func execute(_ method: HTTPMethod = .GET, url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture<Response> {
342334
do {
343-
let request = try Request(url: url, method: .DELETE)
344-
return self.execute(request: request, deadline: deadline, logger: logger)
335+
let request = try Request(url: url, method: method, body: body)
336+
return self.execute(request: request, deadline: deadline, logger: logger ?? HTTPClient.loggingDisabled)
337+
} catch {
338+
return self.eventLoopGroup.next().makeFailedFuture(error)
339+
}
340+
}
341+
342+
/// Execute arbitrary HTTP+UNIX request to a unix domain socket path, using the specified URL as the request to send to the server.
343+
///
344+
/// - parameters:
345+
/// - method: Request method.
346+
/// - socketPath: The path to the unix domain socket to connect to.
347+
/// - urlPath: The URL path and query that will be sent to the server.
348+
/// - body: Request body.
349+
/// - deadline: Point in time by which the request must complete.
350+
/// - logger: The logger to use for this request.
351+
public func execute(_ method: HTTPMethod = .GET, socketPath: String, urlPath: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture<Response> {
352+
do {
353+
guard let url = URL(httpURLWithSocketPath: socketPath, uri: urlPath) else {
354+
throw HTTPClientError.invalidURL
355+
}
356+
let request = try Request(url: url, method: method, body: body)
357+
return self.execute(request: request, deadline: deadline, logger: logger ?? HTTPClient.loggingDisabled)
358+
} catch {
359+
return self.eventLoopGroup.next().makeFailedFuture(error)
360+
}
361+
}
362+
363+
/// Execute arbitrary HTTPS+UNIX request to a unix domain socket path over TLS, using the specified URL as the request to send to the server.
364+
///
365+
/// - parameters:
366+
/// - method: Request method.
367+
/// - secureSocketPath: The path to the unix domain socket to connect to.
368+
/// - urlPath: The URL path and query that will be sent to the server.
369+
/// - body: Request body.
370+
/// - deadline: Point in time by which the request must complete.
371+
/// - logger: The logger to use for this request.
372+
public func execute(_ method: HTTPMethod = .GET, secureSocketPath: String, urlPath: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture<Response> {
373+
do {
374+
guard let url = URL(httpsURLWithSocketPath: secureSocketPath, uri: urlPath) else {
375+
throw HTTPClientError.invalidURL
376+
}
377+
let request = try Request(url: url, method: method, body: body)
378+
return self.execute(request: request, deadline: deadline, logger: logger ?? HTTPClient.loggingDisabled)
345379
} catch {
346380
return self.eventLoopGroup.next().makeFailedFuture(error)
347381
}

Sources/AsyncHTTPClient/HTTPHandler.swift

+32-2
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ extension HTTPClient {
9595
/// Represent HTTP request.
9696
public struct Request {
9797
/// Represent kind of Request
98-
enum Kind {
99-
enum UnixScheme {
98+
enum Kind: Equatable {
99+
enum UnixScheme: Equatable {
100100
case baseURL
101101
case http_unix
102102
case https_unix
@@ -516,6 +516,36 @@ extension URL {
516516
func hasTheSameOrigin(as other: URL) -> Bool {
517517
return self.host == other.host && self.scheme == other.scheme && self.port == other.port
518518
}
519+
520+
/// Initializes a newly created HTTP URL connecting to a unix domain socket path. The socket path is encoded as the URL's host, replacing percent encoding invalid path characters, and will use the "http+unix" scheme.
521+
/// - Parameters:
522+
/// - socketPath: The path to the unix domain socket to connect to.
523+
/// - uri: The URI path and query that will be sent to the server.
524+
public init?(httpURLWithSocketPath socketPath: String, uri: String = "/") {
525+
guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil }
526+
var urlString: String
527+
if uri.hasPrefix("/") {
528+
urlString = "http+unix://\(host)\(uri)"
529+
} else {
530+
urlString = "http+unix://\(host)/\(uri)"
531+
}
532+
self.init(string: urlString)
533+
}
534+
535+
/// Initializes a newly created HTTPS URL connecting to a unix domain socket path over TLS. The socket path is encoded as the URL's host, replacing percent encoding invalid path characters, and will use the "https+unix" scheme.
536+
/// - Parameters:
537+
/// - socketPath: The path to the unix domain socket to connect to.
538+
/// - uri: The URI path and query that will be sent to the server.
539+
public init?(httpsURLWithSocketPath socketPath: String, uri: String = "/") {
540+
guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil }
541+
var urlString: String
542+
if uri.hasPrefix("/") {
543+
urlString = "https+unix://\(host)\(uri)"
544+
} else {
545+
urlString = "https+unix://\(host)/\(uri)"
546+
}
547+
self.init(string: urlString)
548+
}
519549
}
520550

521551
extension HTTPClient {

Tests/AsyncHTTPClientTests/HTTPClientInternalTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ extension HTTPClientInternalTests {
4343
("testUploadStreamingIsCalledOnTaskEL", testUploadStreamingIsCalledOnTaskEL),
4444
("testWeCanActuallyExactlySetTheEventLoops", testWeCanActuallyExactlySetTheEventLoops),
4545
("testTaskPromiseBoundToEL", testTaskPromiseBoundToEL),
46+
("testInternalRequestURI", testInternalRequestURI),
4647
]
4748
}
4849
}

Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift

+27
Original file line numberDiff line numberDiff line change
@@ -959,4 +959,31 @@ class HTTPClientInternalTests: XCTestCase {
959959
XCTAssertTrue(task.futureResult.eventLoop === el2)
960960
XCTAssertNoThrow(try task.wait())
961961
}
962+
963+
func testInternalRequestURI() throws {
964+
let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar")
965+
XCTAssertEqual(request1.kind, .host)
966+
XCTAssertEqual(request1.socketPath, "")
967+
XCTAssertEqual(request1.uri, "/some/path?foo=bar")
968+
969+
let request2 = try Request(url: "https://someserver.com")
970+
XCTAssertEqual(request2.kind, .host)
971+
XCTAssertEqual(request2.socketPath, "")
972+
XCTAssertEqual(request2.uri, "/")
973+
974+
let request3 = try Request(url: "unix:///tmp/file")
975+
XCTAssertEqual(request3.kind, .unixSocket(.baseURL))
976+
XCTAssertEqual(request3.socketPath, "/tmp/file")
977+
XCTAssertEqual(request3.uri, "/")
978+
979+
let request4 = try Request(url: "http+unix://%2Ftmp%2Ffile/file/path")
980+
XCTAssertEqual(request4.kind, .unixSocket(.http_unix))
981+
XCTAssertEqual(request4.socketPath, "/tmp/file")
982+
XCTAssertEqual(request4.uri, "/file/path")
983+
984+
let request5 = try Request(url: "https+unix://%2Ftmp%2Ffile/file/path")
985+
XCTAssertEqual(request5.kind, .unixSocket(.https_unix))
986+
XCTAssertEqual(request5.socketPath, "/tmp/file")
987+
XCTAssertEqual(request5.uri, "/file/path")
988+
}
962989
}

Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

+5
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,11 @@ internal final class HttpBinHandler: ChannelInboundHandler {
439439
headers.add(name: "X-Calling-URI", value: req.uri)
440440
self.resps.append(HTTPResponseBuilder(status: .ok, headers: headers))
441441
return
442+
case "/echo-method":
443+
var headers = self.responseHeaders
444+
headers.add(name: "X-Method-Used", value: req.method.rawValue)
445+
self.resps.append(HTTPResponseBuilder(status: .ok, headers: headers))
446+
return
442447
case "/ok":
443448
self.resps.append(HTTPResponseBuilder(status: .ok))
444449
return

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ extension HTTPClientTests {
2828
("testRequestURI", testRequestURI),
2929
("testBadRequestURI", testBadRequestURI),
3030
("testSchemaCasing", testSchemaCasing),
31+
("testURLSocketPathInitializers", testURLSocketPathInitializers),
32+
("testConvenienceExecuteMethods", testConvenienceExecuteMethods),
33+
("testConvenienceExecuteMethodsOverSocket", testConvenienceExecuteMethodsOverSocket),
34+
("testConvenienceExecuteMethodsOverSecureSocket", testConvenienceExecuteMethodsOverSecureSocket),
3135
("testGet", testGet),
3236
("testGetWithDifferentEventLoopBackpressure", testGetWithDifferentEventLoopBackpressure),
3337
("testPost", testPost),

0 commit comments

Comments
 (0)