Repository navigation
Retrying a multipart request raises "Cannot write to closed encoder" #818
Description
Checklist
- I have looked into the Readme and Examples, and have not found a suitable solution or answer.
- I have looked into the API documentation and have not found a suitable solution or answer.
- I have searched the issues and have not found a suitable solution or answer.
- I have searched the Auth0 Community forums and have not found a suitable solution or answer.
- I agree to the terms within the Auth0 Code of Conduct.
Description
When a multipart request gets a retryable status (429 or 5xx), Internal::Http::RawClient#send retries it and calls request.encode_body again. Internal::Multipart::FormData keeps a single Encoder, which is closed after the first encode, so the second call raises RuntimeError: Cannot write to closed encoder from Encoder#write_part. The retry is never sent.
This affects jobs.users_imports.create, the multipart endpoint we use. Any 429 or 5xx on a users import comes back as a RuntimeError, so callers that rescue Auth0::Errors::ResponseError don't see it.
Expected: the retry sends the same multipart body again, and once retries run out the client raises the API error (Auth0::Errors::ClientError for a 429), as it does for JSON requests.
Reproduction
Reproducible every time with the default management_max_retries:
require "auth0_client"
require "webmock"
require "tempfile"
include WebMock::API
WebMock.enable!
stub_request(:post, "https://example.auth0.com/oauth/token")
.to_return(status: 200, body: { access_token: "t", expires_in: 86_400 }.to_json,
headers: { "Content-Type" => "application/json" })
stub_request(:post, "https://example.auth0.com/api/v2/jobs/users-imports")
.to_return(status: 429, body: { statusCode: 429 }.to_json,
headers: { "Content-Type" => "application/json" })
client = Auth0Client.new(client_id: "id", client_secret: "secret", domain: "example.auth0.com")
file = Tempfile.new("users")
file.write('[{"email":"a@example.com"}]')
file.rewind
client.jobs.users_imports.create(users: file, connection_id: "con_123")
# => lib/auth0/internal/multipart/multipart_encoder.rb:68:in `write_part':
# Cannot write to closed encoder (RuntimeError)With management_max_retries: 0 the same call raises Auth0::Errors::ClientError after one request, as expected.
Additional context
FormData#initializecreates the encoder once (lib/auth0/internal/multipart/multipart_form_data.rb:15) andFormData#encodereuses it (:63).Encoder#encodecloses it at the end.RawClient#sendbuilds the request body inside the retry loop (lib/auth0/internal/http/raw_client.rb:59), so every retry encodes the body again.- The
Content-Typeheader is built from the encoder's boundary inMultipart::Request#encode_headers, so a fix needs to keep the same boundary across attempts. For example, encode the body once per request and reuse the string, or letEncoder#encodereset its buffer instead of failing. - Like Model#initialize drops every
falsevalue — booleans are silently lost on requests and responses #800 / fix: preserve false values in model fields #811, this code lives underlib/auth0/internal/, which is in.fernignore, so a regenerated release won't pick up a generator fix on its own. - Different from import_users job failing on
to_form_data_part#796, which was about building theuserspart and is fixed in 6.5.0. - Our workaround for now: a separate client with
management_max_retries: 0for users imports, so a 429 surfaces asAuth0::Errors::ClientErrorand our background job retries it.
ruby-auth0 version
6.5.0
Ruby version
3.3.9
Activity