Skip to content

Commit 22a985e

Browse files
authored
Merge pull request swiftlang#2838 from agisboye/SR-13027
SR-13027: Fix URL encoding in redirects
2 parents 4caa33a + 9d0bf15 commit 22a985e

File tree

3 files changed

+48
-26
lines changed

3 files changed

+48
-26
lines changed

Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -476,9 +476,9 @@ internal class _HTTPURLProtocol: _NativeProtocol {
476476
// We need this ugly cast in order to be able to support `URLSessionTask.init()`
477477
session.delegateQueue.addOperation {
478478
delegate.urlSession(session, task: self.task!, willPerformHTTPRedirection: response as! HTTPURLResponse, newRequest: request) { [weak self] (request: URLRequest?) in
479-
guard let task = self else { return }
480-
self?.task?.workQueue.async {
481-
task.didCompleteRedirectCallback(request)
479+
guard let self = self else { return }
480+
self.task?.workQueue.async {
481+
self.didCompleteRedirectCallback(request)
482482
}
483483
}
484484
}
@@ -651,7 +651,7 @@ internal extension _HTTPURLProtocol {
651651
//TODO: Do we ever want to redirect for HEAD requests?
652652
func methodAndURL() -> (String, URL)? {
653653
guard
654-
let location = response.value(forHeaderField: .location, response: response),
654+
let location = response.value(forHeaderField: .location),
655655
let targetURL = URL(string: location)
656656
else {
657657
// Can't redirect when there's no location to redirect to.
@@ -689,31 +689,33 @@ internal extension _HTTPURLProtocol {
689689
return request
690690
}
691691

692-
let scheme = request.url?.scheme
693-
let host = request.url?.host
694-
let port = request.url?.port
692+
guard
693+
let fromUrl = fromRequest.url,
694+
var components = URLComponents(url: fromUrl, resolvingAgainstBaseURL: false)
695+
else { return nil }
695696

696-
var components = URLComponents()
697-
components.scheme = scheme
698-
components.host = host
699-
// Use the original port if the new URL does not contain a host
700-
// ie Location: /foo => <original host>:<original port>/Foo
701-
// but Location: newhost/foo will ignore the original port
702-
if targetURL.host == nil {
703-
components.port = port
697+
// If the new URL contains a host, use the host and port from the new URL.
698+
// Otherwise, the host and port from the original URL are used.
699+
if targetURL.host != nil {
700+
components.host = targetURL.host
701+
components.port = targetURL.port
704702
}
705-
//The path must either begin with "/" or be an empty string.
706-
if targetURL.relativeString.first != "/" {
707-
components.path = "/" + targetURL.relativeString
703+
704+
// The path must either begin with "/" or be an empty string.
705+
if targetURL.path.hasPrefix("/") {
706+
components.path = targetURL.path
708707
} else {
709-
components.path = targetURL.relativeString
708+
components.path = "/" + targetURL.path
710709
}
710+
711+
// The query and fragment components are set separately to prevent them from being
712+
// percent encoded again.
713+
components.percentEncodedQuery = targetURL.query
714+
components.percentEncodedFragment = targetURL.fragment
711715

712-
guard let urlString = components.string else { fatalError("Invalid URL") }
713-
request.url = URL(string: urlString)
716+
guard let url = components.url else { fatalError("Invalid URL") }
717+
request.url = url
714718

715-
// Inherit the timeout from the previous request
716-
request.timeoutInterval = fromRequest.timeoutInterval
717719
return request
718720
}
719721
}
@@ -726,10 +728,9 @@ fileprivate extension HTTPURLResponse {
726728
case location = "Location"
727729
}
728730

729-
func value(forHeaderField field: _Field, response: HTTPURLResponse?) -> String? {
731+
func value(forHeaderField field: _Field) -> String? {
730732
let value = field.rawValue
731-
guard let response = response else { fatalError("Response is nil") }
732-
if let location = response.allHeaderFields[value] as? String {
733+
if let location = self.allHeaderFields[value] as? String {
733734
return location
734735
}
735736
return nil

Tests/Foundation/HTTPServer.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,11 @@ public class TestURLSessionServer {
841841
"Content-Encoding: gzip"].joined(separator: _HTTPUtils.CRLF),
842842
bodyData: helloWorld)
843843
}
844+
845+
if uri == "/echo-query" {
846+
let body = request.parameters.map { "\($0.key)=\($0.value)" }.joined(separator: "&")
847+
return try _HTTPResponse(response: .OK, body: body)
848+
}
844849

845850
// Look for /xxx where xxx is a 3digit HTTP code
846851
if uri.hasPrefix("/") && uri.count == 4, let code = Int(String(uri.dropFirst())), code > 0 && code < 1000 {

Tests/Foundation/Tests/TestURLSession.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,21 @@ class TestURLSession: LoopbackServerTest {
748748
d.run(with: url)
749749
waitForExpectations(timeout: 12)
750750
}
751+
752+
func test_httpRedirectionWithEncodedQuery() {
753+
let location = "echo-query%3Fparam%3Dfoo" // "echo-query?param=foo" url encoded
754+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/303?location=\(location)"
755+
let url = URL(string: urlString)!
756+
let d = HTTPRedirectionDataTask(with: expectation(description: "GET \(urlString): with HTTP redirection"))
757+
d.run(with: url)
758+
waitForExpectations(timeout: 12)
759+
760+
if let body = String(data: d.receivedData, encoding: .utf8) {
761+
XCTAssertEqual(body, "param=foo")
762+
} else {
763+
XCTFail("No string body")
764+
}
765+
}
751766

752767
// temporarily disabled (https://bugs.swift.org/browse/SR-5751)
753768
func test_httpRedirectionTimeout() {
@@ -1774,6 +1789,7 @@ class TestURLSession: LoopbackServerTest {
17741789
("test_httpRedirectionWithCompleteRelativePath", test_httpRedirectionWithCompleteRelativePath),
17751790
("test_httpRedirectionWithInCompleteRelativePath", test_httpRedirectionWithInCompleteRelativePath),
17761791
("test_httpRedirectionWithDefaultPort", test_httpRedirectionWithDefaultPort),
1792+
("test_httpRedirectionWithEncodedQuery", test_httpRedirectionWithEncodedQuery),
17771793
("test_httpRedirectionTimeout", test_httpRedirectionTimeout),
17781794
("test_httpRedirectionChainInheritsTimeoutInterval", test_httpRedirectionChainInheritsTimeoutInterval),
17791795
("test_httpRedirectionExceededMaxRedirects", test_httpRedirectionExceededMaxRedirects),

0 commit comments

Comments
 (0)