Skip to content

Commit c4feafd

Browse files
authored
Refactor redirect logic to be reusable for async/await (#522)
* refactor RedirectHandler - `redirectState` is no longer a property of `HTTPClient.Request`. RedirectHandler now stores this state directly and therefore no longer optional. - we no longer count the number of allowed redirects down. Instead the number of redirects is dervied from `self.visited.count` and we compare it to the maxRedirect to check if we git the limit. * `HTTPClient.Configuration.RedirectConfiguration.Configuration` is now called `HTTPClient.Configuration.RedirectConfiguration.Mode` only two `Configuration`s left in the type name * add redirect logger test
1 parent 3d7c42e commit c4feafd

File tree

6 files changed

+312
-113
lines changed

6 files changed

+312
-113
lines changed

Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -504,11 +504,43 @@ public class HTTPClient {
504504
/// - delegate: Delegate to process response parts.
505505
/// - eventLoop: NIO Event Loop preference.
506506
/// - deadline: Point in time by which the request must complete.
507-
public func execute<Delegate: HTTPClientResponseDelegate>(request: Request,
508-
delegate: Delegate,
509-
eventLoop eventLoopPreference: EventLoopPreference,
510-
deadline: NIODeadline? = nil,
511-
logger originalLogger: Logger?) -> Task<Delegate.Response> {
507+
/// - logger: The logger to use for this request.
508+
public func execute<Delegate: HTTPClientResponseDelegate>(
509+
request: Request,
510+
delegate: Delegate,
511+
eventLoop eventLoopPreference: EventLoopPreference,
512+
deadline: NIODeadline? = nil,
513+
logger originalLogger: Logger?
514+
) -> Task<Delegate.Response> {
515+
self._execute(
516+
request: request,
517+
delegate: delegate,
518+
eventLoop: eventLoopPreference,
519+
deadline: deadline,
520+
logger: originalLogger,
521+
redirectState: RedirectState(
522+
self.configuration.redirectConfiguration.mode,
523+
initialURL: request.url.absoluteString
524+
)
525+
)
526+
}
527+
528+
/// Execute arbitrary HTTP request and handle response processing using provided delegate.
529+
///
530+
/// - parameters:
531+
/// - request: HTTP request to execute.
532+
/// - delegate: Delegate to process response parts.
533+
/// - eventLoop: NIO Event Loop preference.
534+
/// - deadline: Point in time by which the request must complete.
535+
/// - logger: The logger to use for this request.
536+
func _execute<Delegate: HTTPClientResponseDelegate>(
537+
request: Request,
538+
delegate: Delegate,
539+
eventLoop eventLoopPreference: EventLoopPreference,
540+
deadline: NIODeadline? = nil,
541+
logger originalLogger: Logger?,
542+
redirectState: RedirectState?
543+
) -> Task<Delegate.Response> {
512544
let logger = (originalLogger ?? HTTPClient.loggingDisabled).attachingRequestInformation(request, requestID: globalRequestID.add(1))
513545
let taskEL: EventLoop
514546
switch eventLoopPreference.preference {
@@ -543,22 +575,20 @@ public class HTTPClient {
543575
return failedTask
544576
}
545577

546-
let redirectHandler: RedirectHandler<Delegate.Response>?
547-
switch self.configuration.redirectConfiguration.configuration {
548-
case .follow(let max, let allowCycles):
549-
var request = request
550-
if request.redirectState == nil {
551-
request.redirectState = .init(count: max, visited: allowCycles ? nil : Set())
578+
let redirectHandler: RedirectHandler<Delegate.Response>? = {
579+
guard let redirectState = redirectState else { return nil }
580+
581+
return .init(request: request, redirectState: redirectState) { newRequest, newRedirectState in
582+
self._execute(
583+
request: newRequest,
584+
delegate: delegate,
585+
eventLoop: eventLoopPreference,
586+
deadline: deadline,
587+
logger: logger,
588+
redirectState: newRedirectState
589+
)
552590
}
553-
redirectHandler = RedirectHandler<Delegate.Response>(request: request) { newRequest in
554-
self.execute(request: newRequest,
555-
delegate: delegate,
556-
eventLoop: eventLoopPreference,
557-
deadline: deadline)
558-
}
559-
case .disallow:
560-
redirectHandler = nil
561-
}
591+
}()
562592

563593
let task = Task<Delegate.Response>(eventLoop: taskEL, logger: logger)
564594
do {
@@ -804,21 +834,21 @@ extension HTTPClient.Configuration {
804834

805835
/// Specifies redirect processing settings.
806836
public struct RedirectConfiguration {
807-
enum Configuration {
837+
enum Mode {
808838
/// Redirects are not followed.
809839
case disallow
810840
/// Redirects are followed with a specified limit.
811841
case follow(max: Int, allowCycles: Bool)
812842
}
813843

814-
var configuration: Configuration
844+
var mode: Mode
815845

816846
init() {
817-
self.configuration = .follow(max: 5, allowCycles: false)
847+
self.mode = .follow(max: 5, allowCycles: false)
818848
}
819849

820-
init(configuration: Configuration) {
821-
self.configuration = configuration
850+
init(configuration: Mode) {
851+
self.mode = configuration
822852
}
823853

824854
/// Redirects are not followed.

Sources/AsyncHTTPClient/HTTPHandler.swift

Lines changed: 33 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,9 @@ extension HTTPClient {
109109
/// Request-specific TLS configuration, defaults to no request-specific TLS configuration.
110110
public var tlsConfiguration: TLSConfiguration?
111111

112-
struct RedirectState {
113-
var count: Int
114-
var visited: Set<URL>?
115-
}
116-
117112
/// Parsed, validated and deconstructed URL.
118113
let deconstructedURL: DeconstructedURL
119114

120-
var redirectState: RedirectState?
121-
122115
/// Create HTTP request.
123116
///
124117
/// - parameters:
@@ -190,7 +183,6 @@ extension HTTPClient {
190183
public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws {
191184
self.deconstructedURL = try DeconstructedURL(url: url)
192185

193-
self.redirectState = nil
194186
self.url = url
195187
self.method = method
196188
self.headers = headers
@@ -642,87 +634,42 @@ internal struct TaskCancelEvent {}
642634

643635
internal struct RedirectHandler<ResponseType> {
644636
let request: HTTPClient.Request
645-
let execute: (HTTPClient.Request) -> HTTPClient.Task<ResponseType>
646-
647-
func redirectTarget(status: HTTPResponseStatus, headers: HTTPHeaders) -> URL? {
648-
switch status {
649-
case .movedPermanently, .found, .seeOther, .notModified, .useProxy, .temporaryRedirect, .permanentRedirect:
650-
break
651-
default:
652-
return nil
653-
}
654-
655-
guard let location = headers.first(name: "Location") else {
656-
return nil
657-
}
658-
659-
guard let url = URL(string: location, relativeTo: request.url) else {
660-
return nil
661-
}
662-
663-
guard self.request.deconstructedURL.scheme.supportsRedirects(to: url.scheme) else {
664-
return nil
665-
}
666-
667-
if url.isFileURL {
668-
return nil
669-
}
670-
671-
return url.absoluteURL
637+
let redirectState: RedirectState
638+
let execute: (HTTPClient.Request, RedirectState) -> HTTPClient.Task<ResponseType>
639+
640+
func redirectTarget(status: HTTPResponseStatus, responseHeaders: HTTPHeaders) -> URL? {
641+
responseHeaders.extractRedirectTarget(
642+
status: status,
643+
originalURL: self.request.url,
644+
originalScheme: self.request.deconstructedURL.scheme
645+
)
672646
}
673647

674-
func redirect(status: HTTPResponseStatus, to redirectURL: URL, promise: EventLoopPromise<ResponseType>) {
675-
var nextState: HTTPClient.Request.RedirectState?
676-
if var state = request.redirectState {
677-
guard state.count > 0 else {
678-
return promise.fail(HTTPClientError.redirectLimitReached)
679-
}
680-
681-
state.count -= 1
682-
683-
if var visited = state.visited {
684-
guard !visited.contains(redirectURL) else {
685-
return promise.fail(HTTPClientError.redirectCycleDetected)
686-
}
687-
688-
visited.insert(redirectURL)
689-
state.visited = visited
690-
}
691-
692-
nextState = state
693-
}
694-
695-
let originalRequest = self.request
696-
697-
var convertToGet = false
698-
if status == .seeOther, self.request.method != .HEAD {
699-
convertToGet = true
700-
} else if status == .movedPermanently || status == .found, self.request.method == .POST {
701-
convertToGet = true
702-
}
703-
704-
var method = originalRequest.method
705-
var headers = originalRequest.headers
706-
var body = originalRequest.body
707-
708-
if convertToGet {
709-
method = .GET
710-
body = nil
711-
headers.remove(name: "Content-Length")
712-
headers.remove(name: "Content-Type")
713-
}
714-
715-
if !originalRequest.url.hasTheSameOrigin(as: redirectURL) {
716-
headers.remove(name: "Origin")
717-
headers.remove(name: "Cookie")
718-
headers.remove(name: "Authorization")
719-
headers.remove(name: "Proxy-Authorization")
720-
}
721-
648+
func redirect(
649+
status: HTTPResponseStatus,
650+
to redirectURL: URL,
651+
promise: EventLoopPromise<ResponseType>
652+
) {
722653
do {
723-
var newRequest = try HTTPClient.Request(url: redirectURL, method: method, headers: headers, body: body)
724-
newRequest.redirectState = nextState
725-
self.execute(newRequest).futureResult.whenComplete { result in
654+
var redirectState = self.redirectState
655+
try redirectState.redirect(to: redirectURL.absoluteString)
656+
657+
let (method, headers, body) = transformRequestForRedirect(
658+
from: request.url,
659+
method: self.request.method,
660+
headers: self.request.headers,
661+
body: self.request.body,
662+
to: redirectURL,
663+
status: status
664+
)
665+
666+
let newRequest = try HTTPClient.Request(
667+
url: redirectURL,
668+
method: method,
669+
headers: headers,
670+
body: body
671+
)
672+
self.execute(newRequest, redirectState).futureResult.whenComplete { result in
726673
promise.futureResult.eventLoop.execute {
727674
promise.completeWith(result)
728675
}

0 commit comments

Comments
 (0)