Skip to content

Commit 9d8cd95

Browse files
authored
[Redirect] Allow redirect response to have body (swift-server#580)
### 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 swift-server#574
1 parent 0a2004b commit 9d8cd95

File tree

4 files changed

+295
-32
lines changed

4 files changed

+295
-32
lines changed

Sources/AsyncHTTPClient/RequestBag+StateMachine.swift

+65-14
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,24 @@ import struct Foundation.URL
1616
import NIOCore
1717
import NIOHTTP1
1818

19+
extension HTTPClient {
20+
/// The maximum body size allowed, before a redirect response is cancelled. 3KB.
21+
///
22+
/// Why 3KB? We feel like this is a good compromise between potentially reusing the
23+
/// connection in HTTP/1.1 mode (if we load all data from the redirect response we can
24+
/// reuse the connection) and not being to wasteful in the amount of data that is thrown
25+
/// away being transferred.
26+
fileprivate static let maxBodySizeRedirectResponse = 1024 * 3
27+
}
28+
1929
extension RequestBag {
2030
struct StateMachine {
2131
fileprivate enum State {
2232
case initialized
2333
case queued(HTTPRequestScheduler)
2434
case executing(HTTPRequestExecutor, RequestStreamState, ResponseStreamState)
2535
case finished(error: Error?)
26-
case redirected(HTTPResponseHead, URL)
36+
case redirected(HTTPRequestExecutor, Int, HTTPResponseHead, URL)
2737
case modifying
2838
}
2939

@@ -259,11 +269,18 @@ extension RequestBag.StateMachine {
259269
}
260270
}
261271

272+
enum ReceiveResponseHeadAction {
273+
case none
274+
case forwardResponseHead(HTTPResponseHead)
275+
case signalBodyDemand(HTTPRequestExecutor)
276+
case redirect(HTTPRequestExecutor, RedirectHandler<Delegate.Response>, HTTPResponseHead, URL)
277+
}
278+
262279
/// The response head has been received.
263280
///
264281
/// - Parameter head: The response' head
265282
/// - 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 {
283+
mutating func receiveResponseHead(_ head: HTTPResponseHead) -> ReceiveResponseHeadAction {
267284
switch self.state {
268285
case .initialized, .queued:
269286
preconditionFailure("How can we receive a response, if the request hasn't started yet.")
@@ -276,24 +293,40 @@ extension RequestBag.StateMachine {
276293
status: head.status,
277294
responseHeaders: head.headers
278295
) {
279-
self.state = .redirected(head, redirectURL)
280-
return false
296+
// If we will redirect, we need to consume the response's body ASAP, to be able to
297+
// reuse the existing connection. We will consume a response body, if the body is
298+
// smaller than 3kb.
299+
switch head.contentLength {
300+
case .some(0...(HTTPClient.maxBodySizeRedirectResponse)), .none:
301+
self.state = .redirected(executor, 0, head, redirectURL)
302+
return .signalBodyDemand(executor)
303+
case .some:
304+
self.state = .finished(error: HTTPClientError.cancelled)
305+
return .redirect(executor, self.redirectHandler!, head, redirectURL)
306+
}
281307
} else {
282308
self.state = .executing(executor, requestState, .buffering(.init(), next: .askExecutorForMore))
283-
return true
309+
return .forwardResponseHead(head)
284310
}
285311
case .redirected:
286312
preconditionFailure("This state can only be reached after we have received a HTTP head")
287313
case .finished(error: .some):
288-
return false
314+
return .none
289315
case .finished(error: .none):
290316
preconditionFailure("How can the request be finished without error, before receiving response head?")
291317
case .modifying:
292318
preconditionFailure("Invalid state: \(self.state)")
293319
}
294320
}
295321

296-
mutating func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>) -> ByteBuffer? {
322+
enum ReceiveResponseBodyAction {
323+
case none
324+
case forwardResponsePart(ByteBuffer)
325+
case signalBodyDemand(HTTPRequestExecutor)
326+
case redirect(HTTPRequestExecutor, RedirectHandler<Delegate.Response>, HTTPResponseHead, URL)
327+
}
328+
329+
mutating func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>) -> ReceiveResponseBodyAction {
297330
switch self.state {
298331
case .initialized, .queued:
299332
preconditionFailure("How can we receive a response body part, if the request hasn't started yet.")
@@ -312,17 +345,26 @@ extension RequestBag.StateMachine {
312345
currentBuffer.append(contentsOf: buffer)
313346
}
314347
self.state = .executing(executor, requestState, .buffering(currentBuffer, next: next))
315-
return nil
348+
return .none
316349
case .executing(let executor, let requestState, .waitingForRemote):
317350
var buffer = buffer
318351
let first = buffer.removeFirst()
319352
self.state = .executing(executor, requestState, .buffering(buffer, next: .askExecutorForMore))
320-
return first
321-
case .redirected:
322-
// ignore body
323-
return nil
353+
return .forwardResponsePart(first)
354+
case .redirected(let executor, var receivedBytes, let head, let redirectURL):
355+
let partsLength = buffer.reduce(into: 0) { $0 += $1.readableBytes }
356+
receivedBytes += partsLength
357+
358+
if receivedBytes > HTTPClient.maxBodySizeRedirectResponse {
359+
self.state = .finished(error: HTTPClientError.cancelled)
360+
return .redirect(executor, self.redirectHandler!, head, redirectURL)
361+
} else {
362+
self.state = .redirected(executor, receivedBytes, head, redirectURL)
363+
return .signalBodyDemand(executor)
364+
}
365+
324366
case .finished(error: .some):
325-
return nil
367+
return .none
326368
case .finished(error: .none):
327369
preconditionFailure("How can the request be finished without error, before receiving response head?")
328370
case .modifying:
@@ -368,7 +410,7 @@ extension RequestBag.StateMachine {
368410
self.state = .executing(executor, requestState, .buffering(newChunks, next: .eof))
369411
return .consume(first)
370412

371-
case .redirected(let head, let redirectURL):
413+
case .redirected(_, _, let head, let redirectURL):
372414
self.state = .finished(error: nil)
373415
return .redirect(self.redirectHandler!, head, redirectURL)
374416

@@ -529,3 +571,12 @@ extension RequestBag.StateMachine {
529571
}
530572
}
531573
}
574+
575+
extension HTTPResponseHead {
576+
var contentLength: Int? {
577+
guard let header = self.headers.first(name: "content-length") else {
578+
return nil
579+
}
580+
return Int(header)
581+
}
582+
}

Sources/AsyncHTTPClient/RequestBag.swift

+34-18
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

+3
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)