Skip to content

Commit d2c0583

Browse files
committed
[Redirect] Allow redirect response to have body
### Motivation Currently, we don’t consume the response body of redirect responses. Because of this requests, that receive a redirect response with response body, may hang indefinitely. ### Changes - Consume redirect response body if less than 3kb - Cancel redirect response if larger than 3kb ### Result Redirect responses are consumed. Fixes #574
1 parent 0a2004b commit d2c0583

File tree

4 files changed

+291
-32
lines changed

4 files changed

+291
-32
lines changed

Sources/AsyncHTTPClient/RequestBag+StateMachine.swift

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@ import struct Foundation.URL
1616
import NIOCore
1717
import NIOHTTP1
1818

19+
extension HTTPClient {
20+
fileprivate static let maxBodySizeRedirectResponse = 1024 * 3
21+
}
22+
1923
extension RequestBag {
2024
struct StateMachine {
25+
/// The maximum body size allowed, before a redirect response is cancelled.
26+
2127
fileprivate enum State {
2228
case initialized
2329
case queued(HTTPRequestScheduler)
2430
case executing(HTTPRequestExecutor, RequestStreamState, ResponseStreamState)
2531
case finished(error: Error?)
26-
case redirected(HTTPResponseHead, URL)
32+
case redirected(HTTPRequestExecutor, Int, HTTPResponseHead, URL)
2733
case modifying
2834
}
2935

@@ -259,11 +265,18 @@ extension RequestBag.StateMachine {
259265
}
260266
}
261267

268+
enum ReceiveResponseHeadAction {
269+
case none
270+
case forwardResponseHead(HTTPResponseHead)
271+
case signalBodyDemand(HTTPRequestExecutor)
272+
case redirect(HTTPRequestExecutor, RedirectHandler<Delegate.Response>, HTTPResponseHead, URL)
273+
}
274+
262275
/// The response head has been received.
263276
///
264277
/// - Parameter head: The response' head
265278
/// - Returns: Whether the response should be forwarded to the delegate. Will be `false` if the request follows a redirect.
266-
mutating func receiveResponseHead(_ head: HTTPResponseHead) -> Bool {
279+
mutating func receiveResponseHead(_ head: HTTPResponseHead) -> ReceiveResponseHeadAction {
267280
switch self.state {
268281
case .initialized, .queued:
269282
preconditionFailure("How can we receive a response, if the request hasn't started yet.")
@@ -276,24 +289,40 @@ extension RequestBag.StateMachine {
276289
status: head.status,
277290
responseHeaders: head.headers
278291
) {
279-
self.state = .redirected(head, redirectURL)
280-
return false
292+
// If we will redirect, we need to consume the response's body ASAP, to be able to
293+
// reuse the existing connection. We will consume a response body, if the body is
294+
// smaller than 3kb.
295+
switch head.contentLength {
296+
case .some(0...(HTTPClient.maxBodySizeRedirectResponse)), .none:
297+
self.state = .redirected(executor, 0, head, redirectURL)
298+
return .signalBodyDemand(executor)
299+
case .some:
300+
self.state = .finished(error: HTTPClientError.cancelled)
301+
return .redirect(executor, self.redirectHandler!, head, redirectURL)
302+
}
281303
} else {
282304
self.state = .executing(executor, requestState, .buffering(.init(), next: .askExecutorForMore))
283-
return true
305+
return .forwardResponseHead(head)
284306
}
285307
case .redirected:
286308
preconditionFailure("This state can only be reached after we have received a HTTP head")
287309
case .finished(error: .some):
288-
return false
310+
return .none
289311
case .finished(error: .none):
290312
preconditionFailure("How can the request be finished without error, before receiving response head?")
291313
case .modifying:
292314
preconditionFailure("Invalid state: \(self.state)")
293315
}
294316
}
295317

296-
mutating func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>) -> ByteBuffer? {
318+
enum ReceiveResponseBodyAction {
319+
case none
320+
case forwardResponsePart(ByteBuffer)
321+
case signalBodyDemand(HTTPRequestExecutor)
322+
case redirect(HTTPRequestExecutor, RedirectHandler<Delegate.Response>, HTTPResponseHead, URL)
323+
}
324+
325+
mutating func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>) -> ReceiveResponseBodyAction {
297326
switch self.state {
298327
case .initialized, .queued:
299328
preconditionFailure("How can we receive a response body part, if the request hasn't started yet.")
@@ -312,17 +341,26 @@ extension RequestBag.StateMachine {
312341
currentBuffer.append(contentsOf: buffer)
313342
}
314343
self.state = .executing(executor, requestState, .buffering(currentBuffer, next: next))
315-
return nil
344+
return .none
316345
case .executing(let executor, let requestState, .waitingForRemote):
317346
var buffer = buffer
318347
let first = buffer.removeFirst()
319348
self.state = .executing(executor, requestState, .buffering(buffer, next: .askExecutorForMore))
320-
return first
321-
case .redirected:
322-
// ignore body
323-
return nil
349+
return .forwardResponsePart(first)
350+
case .redirected(let executor, var receivedBytes, let head, let redirectURL):
351+
let partsLength = buffer.reduce(into: 0) { $0 += $1.readableBytes }
352+
receivedBytes += partsLength
353+
354+
if receivedBytes > HTTPClient.maxBodySizeRedirectResponse {
355+
self.state = .finished(error: HTTPClientError.cancelled)
356+
return .redirect(executor, self.redirectHandler!, head, redirectURL)
357+
} else {
358+
self.state = .redirected(executor, receivedBytes, head, redirectURL)
359+
return .signalBodyDemand(executor)
360+
}
361+
324362
case .finished(error: .some):
325-
return nil
363+
return .none
326364
case .finished(error: .none):
327365
preconditionFailure("How can the request be finished without error, before receiving response head?")
328366
case .modifying:
@@ -368,7 +406,7 @@ extension RequestBag.StateMachine {
368406
self.state = .executing(executor, requestState, .buffering(newChunks, next: .eof))
369407
return .consume(first)
370408

371-
case .redirected(let head, let redirectURL):
409+
case .redirected(_, _, let head, let redirectURL):
372410
self.state = .finished(error: nil)
373411
return .redirect(self.redirectHandler!, head, redirectURL)
374412

@@ -529,3 +567,12 @@ extension RequestBag.StateMachine {
529567
}
530568
}
531569
}
570+
571+
extension HTTPResponseHead {
572+
var contentLength: Int? {
573+
guard let header = self.headers.first(name: "content-length") else {
574+
return nil
575+
}
576+
return Int(header)
577+
}
578+
}

Sources/AsyncHTTPClient/RequestBag.swift

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -196,33 +196,49 @@ final class RequestBag<Delegate: HTTPClientResponseDelegate> {
196196
self.task.eventLoop.assertInEventLoop()
197197

198198
// runs most likely on channel eventLoop
199-
let forwardToDelegate = self.state.receiveResponseHead(head)
199+
switch self.state.receiveResponseHead(head) {
200+
case .none:
201+
break
200202

201-
guard forwardToDelegate else { return }
203+
case .signalBodyDemand(let executor):
204+
executor.demandResponseBodyStream(self)
202205

203-
self.delegate.didReceiveHead(task: self.task, head)
204-
.hop(to: self.task.eventLoop)
205-
.whenComplete { result in
206-
// After the head received, let's start to consume body data
207-
self.consumeMoreBodyData0(resultOfPreviousConsume: result)
208-
}
206+
case .redirect(let executor, let handler, let head, let newURL):
207+
handler.redirect(status: head.status, to: newURL, promise: self.task.promise)
208+
executor.cancelRequest(self)
209+
210+
case .forwardResponseHead(let head):
211+
self.delegate.didReceiveHead(task: self.task, head)
212+
.hop(to: self.task.eventLoop)
213+
.whenComplete { result in
214+
// After the head received, let's start to consume body data
215+
self.consumeMoreBodyData0(resultOfPreviousConsume: result)
216+
}
217+
}
209218
}
210219

211220
private func receiveResponseBodyParts0(_ buffer: CircularBuffer<ByteBuffer>) {
212221
self.task.eventLoop.assertInEventLoop()
213222

214-
let maybeForwardBuffer = self.state.receiveResponseBodyParts(buffer)
223+
switch self.state.receiveResponseBodyParts(buffer) {
224+
case .none:
225+
break
215226

216-
guard let forwardBuffer = maybeForwardBuffer else {
217-
return
218-
}
227+
case .signalBodyDemand(let executor):
228+
executor.demandResponseBodyStream(self)
219229

220-
self.delegate.didReceiveBodyPart(task: self.task, forwardBuffer)
221-
.hop(to: self.task.eventLoop)
222-
.whenComplete { result in
223-
// on task el
224-
self.consumeMoreBodyData0(resultOfPreviousConsume: result)
225-
}
230+
case .redirect(let executor, let handler, let head, let newURL):
231+
handler.redirect(status: head.status, to: newURL, promise: self.task.promise)
232+
executor.cancelRequest(self)
233+
234+
case .forwardResponsePart(let part):
235+
self.delegate.didReceiveBodyPart(task: self.task, part)
236+
.hop(to: self.task.eventLoop)
237+
.whenComplete { result in
238+
// on task el
239+
self.consumeMoreBodyData0(resultOfPreviousConsume: result)
240+
}
241+
}
226242
}
227243

228244
private func succeedRequest0(_ buffer: CircularBuffer<ByteBuffer>?) {

Tests/AsyncHTTPClientTests/RequestBagTests+XCTest.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ extension RequestBagTests {
3434
("testChannelBecomingWritableDoesntCrashCancelledTask", testChannelBecomingWritableDoesntCrashCancelledTask),
3535
("testHTTPUploadIsCancelledEvenThoughRequestSucceeds", testHTTPUploadIsCancelledEvenThoughRequestSucceeds),
3636
("testRaceBetweenConnectionCloseAndDemandMoreData", testRaceBetweenConnectionCloseAndDemandMoreData),
37+
("testRedirectWith3KBBody", testRedirectWith3KBBody),
38+
("testRedirectWith4KBBodyAnnouncedInResponseHead", testRedirectWith4KBBodyAnnouncedInResponseHead),
39+
("testRedirectWith4KBBodyNotAnnouncedInResponseHead", testRedirectWith4KBBodyNotAnnouncedInResponseHead),
3740
]
3841
}
3942
}

0 commit comments

Comments
 (0)