-
Notifications
You must be signed in to change notification settings - Fork 130
Support the multipart content type #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
[Generator] Integrate the new URI and String coders ### Motivation Depends on runtime changes from apple/swift-openapi-runtime#45. Up until now, we relied on a series of marker and helper protocols `_StringConvertible` and `_AutoLosslessStringConvertible` to handle converting between various types and their string representation. This has been very manual and required a non-trivial amount of work to support any extra type, especially `Date` and generated string enums. Well, turns out this was an unnecessarily difficult way to approach the problem - we had a better solution available for a long time - `Codable`. Since all the generated types and all the built-in types we reference are already `Codable`, there is no need to reinvent a way to serialize and deserialize types, and we should just embrace it. While a JSON encoder and decoder already exists in Foundation, we didn't have one handy for encoding to and from URIs (used by headers, query and path parameters), and raw string representation (using `LosslessStringConvertible`). We created those in the runtime library in PRs apple/swift-openapi-runtime#44 and apple/swift-openapi-runtime#41, and integrated them into our helper functions (which got significantly simplified this way) in apple/swift-openapi-runtime#45. Out of scope of this PR, but this also opens the door to supporting URL form encoded bodies (#182), multipart (#36), and base64 (#11). While this should be mostly invisible to our adopters, this refactoring creates space for implementing more complex features and overall simplifies our serialization story. ### Modifications - Updated the generator to use the new helper functions. - Updated the article about serialization, shows how we reduced the number of helper functions by moving to `Codable`. - Set the `lineLength` to 120 on the formatter configuration, it was inconsistent with our `.swift-format` file, and lead to the soundness script trying to update the reference files, but then the reference tests were failing. Since we're planning to sync these in #40, this is a step closer to it, but it means that it's probably best to review this PR's diff with whitespace ignored. ### Result Now the generated code uses the new helper functions, allowing us to delete all the deprecated helpers in 0.2.0. ### Test Plan Updated file-based reference, snippet, and unit tests. Reviewed by: glbrntt Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #226
This one needs a bit of design, but should be possible. A simple idea:
|
Additional reading: https://datatracker.ietf.org/doc/html/rfc7578 |
There are also headers associated with each part, not just content type + payload: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#encoding-object-example |
Can also be represented as an array property at the top level, at which point it could lend itself well to being a stream of values? |
Should the parts be available as they become available, or all at the end in the generated struct? Seems we have to decide between more type-safety and delivering parts as they're ready instead of only returning them all at the end. |
I'm working on this now, is taking a bit of time as it's a relatively large feature. A proposal with the new API/generated code is coming hopefully next week. In the meantime, please drop here your use cases of multipart, such as:
Thanks! 🙏 |
Hi @czechboy0 thank you for taking on this issue, as I'm currently running into a related issue with the generated client while performing a multipart upload. I'm writing code that updates a user's profile photo, they are submitting the photo let res = try await client.post_sol_users_sol__lcub_userId_rcub__sol_photo(
.init(path: .init(userId: user.userId),
headers: .init(),
body: .multipartForm(data) // image data here
)
) Unfortunately the server is producing an error when I attempt to complete this operation:
This makes sense, as the client currently hardcodes the content type to request.body = try converter.setOptionalRequestBodyAsBinary(
value,
headerFields: &request.headerFields,
contentType: "multipart/form-data"
) Even without fully fleshing out the multipart spec as outlined above, it would be great to address this in a point release of the client. Thank you for taking the time to work on this! |
unfortunately this is expected, as what you're sending is not a valid multipart request. While I hope to have a proposal up for multipart support this week, and hopefully have everything landed by end of next week, here's a temporary workaround:
Lmk if you need more details here. |
More details. In your middleware, you'd modify the header from
to
And then the body would need to look something like:
Note that each newline needs to be a CRLF, not just a simple newline. Hopefully this should be accepted by the server and can serve as a workaround for the new few weeks before proper multipart support lands. |
Thanks for the suggestions @czechboy0! I found a workaround that is okay for the time being. I changed the upload parameters to accept an requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary This only works because in my particular case I don't have other metadata to upload in the body, so I will keep watching progress on this issue. |
Oh yeah if you only have a single payload, don't use multipart, use "single"part 🙂 (what you're doing, just one body payload). |
Proposal: #369 Please, have a read and chime in with questions, comments. |
All the runtime changes landed in main, will be released in 1.0.0-alpha.1 next week. Generator changes are in a PR: #366. |
### Motivation Implement SOAR-0009. Fixes #36. ### Modifications It's a large diff, and adds some complexity, however I tried to avoid needlessly complicating existing code and opted for duplicating parts of the code specifically for multipart, for easier reasoning. The implementation very much maps to the changes described in the proposal, which I won't repeat here. Some notable highlights before you dive in: - Testing is mainly done through snippet tests, and I added one request and one response operation to make sure things work end to end. Maybe review those first. - The logic for generating request and response bodies got slightly extended to detect when we're generating multipart content, and branches off to bespoke code specifically for multipart, all in the new `Multipart` directory. - Made a few related changes, but tried to keep this isolated to multipart as much as reasonable. - Let me know which parts I should elaborate on, I'm happy to explain in more detail. ### Result Multipart content now works, as proposed in SOAR-0009. ### Test Plan Added a bunch of snippet tests and two new file-based reference test operations, including petstore consumer tests to verify that it all works at runtime as well.
Runtime changes landed, all going out next week. |
@czechboy0 is there a way to track upload progress of a multipart request? |
Yes, a few - using a middleware or using a wrapping async sequence. Before I recommend more details, can you elaborate? Do you want to track the upload progress of the full body (which includes potentially multiple multipart parts), or just the one part? What is the code for uploading it, how are you providing the data? |
I'm uploading a large video and a short text. Upload progress of the text content (description) is negligible - I don't care if it is included in overall progres.
|
I haven't tested that this works, but you could use NIO's FileSystem module (https://github.com/apple/swift-nio/blob/main/Sources/NIOFileSystem/Docs.docc/NIOFileSystem.md) like this. It has the advantage of never loading the full video into memory at once, instead streaming it from disk and printing a log line on every chunk update. import OpenAPIRuntime
import Foundation
import OpenAPIURLSession
import NIOFileSystem
let client = Client(
serverURL: URL(string: "https://example.com/api")!,
transport: URLSessionTransport()
)
actor ProgressLogger {
private let totalSize: Int64
private var accumulatedSize: Int64
init(totalSize: Int64) {
self.totalSize = totalSize
self.accumulatedSize = 0
}
func processChunkSize(_ size: Int) {
accumulatedSize += Int64(size)
print("Progress: \(accumulatedSize)/\(totalSize)")
}
}
try await FileSystem.shared.withFileHandle(forReadingAt: FilePath("/foo/video.mp4")) { read in
let chunks = read.readChunks()
let size = try await read.fileHandle.info().size
let progress = ProgressLogger(totalSize: size)
_ = try await client
.upload(
body: .multipartForm(
[
.content(.init(payload: .init(body: "this is a description"))),
.video(
.init(
payload: .init(body: HTTPBody(
chunks
.map { ArraySlice($0.readableBytesView) }
.map { chunk in
await progress.processChunkSize(chunk.count)
return chunk
},
length: .known(size),
iterationBehavior: .single
)),
filename: "video.mp4"
)
)
]
)
)
.accepted
} |
That worked with almost no changes! Thanks a lot. This should be documented somewhere in readme 🙏 |
Uh oh!
There was an error while loading. Please reload this page.
https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#special-considerations-for-multipart-content
Let's do a proposal first, as there are a few ways the generated code could go here.
The text was updated successfully, but these errors were encountered: