swift-openapi-generator: How to deal with TooManyIterationsError since Swift OpenAPI URLSession 0.3.1?
Hello,
The library Swift OpenAPI URLSession, since its version 0.3.1, contains the commit 686df72 which has the URLSession.bidirectionalStreamingRequest()
method return a HTTPBody
created with the .single
iteration behavior.
This behavior is still present in 1.0.0-alpha.1.
Since my application has upgraded to Swift OpenAPI URLSession 0.3.1, this creates an issue with a ClientMiddleware
of mine, which consumes the body in order to log it.
The reason why I log the body is because it is very useful for debugging the server, the openapi.yml, and generally the decoding of success and error responses. Sometimes this log is the only way to have strict verbatim of whatâs happening, especially for requests that canât be replaid.
So this logging middleware has started failing:
struct MyLogger: ClientMiddleware {
public func intercept(
_ request: HTTPRequest,
body requestBody: HTTPBody?,
baseURL: URL,
operationID: String,
next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?)
{
let (response, responseBody) = try await next(request, requestBody, baseURL)
// NEW in v0.3.1: â OpenAPIRuntime.HTTPBody attempted to create a
// second iterator, but the underlying sequence is only safe to be
// iterated once.
if let responseBody,
let bodyData = try? await Data(collecting: responseBody, upTo: 1024 * 1024)
{
// log the data with other relevant information
}
return (response, responseBody)
}
}
What are my options, if I want to restore body logging?
About this issue
- Original URL
- State: closed
- Created 7 months ago
- Comments: 15
Hurry up, because 1.0.0 is almost there đ
I wonât oppose this point of view, but I may try to explain mine.
When I first wrote my logger middleware, the response body was not async yet (v0.2.0). I was surprised that a middleware could not process the HTTP response until the whole data has been downloaded, long after the status code and headers were received, but I wasnât worried:
Indeed, when I upgraded to v0.3.0, the response body turned async as expected. This time I was surprised that this stream could be awaited twice (once by my middleware, and once by the generated OpenAPI code). Anyway, this worked, so I didnât bother more than a few seconds:
At last, with v0.3.1, I got the
TooManyIterationsError
âOpenAPIRuntime.HTTPBody attempted to create a second iteratorâ.I first struggled to fix it, because I hadnât learnt about
HTTPBody
constructors yet. Hence this issue.But I want to stress out that itâs only with this error that everything made sense at last. All my initial surprises were fixed.
This is why Iâm not sure it is a good idea to make
HTTPBody
an enum that screams itsiterationBehavior
with its cases (if I understood well the suggestion). It looks like an implementation detail to me. Efficient HTTP handling requires single-pass sequences, so all middlewares in the chain should be ready to deal with them. OpenAPI generator should help them doing it right.Even if
HTTPBody
was turned in to an enum, no case would prevent me from buffering the response body. I suppose it would not become forbidden to log responses from a single-pass sequence, right?So the initial problem would still be there, which is that nothing prevents a middleware from returning a response body that was consumed:
Thatâs why a
HTTPResponse: ~Copyable
, as suggested by @czechboy0, may well be a better solution. It would make it impossible to return the sameresponseBody
after consuming it:Iâm not expert of non-copyable types though, so Iâm unable yet to foresee how difficult it would become to implement
StreamingBodyLoggingMiddleware
, or a semi-buffered variant of it that yields valid UTF-8 chunks, on top of this new pickyHTTPResponse
. I mean that preventing invalid code is good, but maybe not so good if it makes it very difficult to deal with expectable valid use cases.Thanks for confirming! đ
I agree with you that everything starts from robust low-level components anyway, regardless of eventual conveniences that are added later.
You could create a
RequestIdInjectingMiddleware
that adds a header to the request, and other middlewares can look for it there. But otherwise no, the runtime library code doesnât insert any unique identifier through other channels.For logging chunks, and preventing buffering, weâre also considering this: https://github.com/apple/swift-openapi-runtime/pull/87
Iâm sorry, this was a wrong alert.
For whoever has the same issue, make sure you return a new
HTTPBody
instance built from the collected data. This one can be iterated as many times as needed: