Hi,
We upload files to users' Dropbox accounts from our server using /2/files/upload with short-lived access tokens and refresh tokens (token_access_type=offline). We refresh the token when we receive 401 with "expired_access_token" and then retry the request.
We found that when the access token has already expired, the upload request sometimes fails with a connection reset (ECONNRESET) while we are still sending the request body, instead of returning 401. Because we never receive the 401, our refresh logic is never triggered, and every later upload with the same token fails the same way until some other request happens to receive the 401 and refreshes the token.
Environment
- Client: Ruby 3.4 / Net::HTTP (no external HTTP library)
- Request: POST https://content.dropboxapi.com/2/files/upload
- Headers: Authorization: Bearer <token>, Dropbox-API-Arg, Content-Type: application/octet-stream, Content-Length (fixed-length body, no chunked encoding, no "Expect: 100-continue")
- The body is streamed from an IO object
- Server location: AWS ap-northeast, running on ECS
What we observe in production
- The upload fails about 1.5–1.8 seconds after it starts. The exception is raised while writing the body (OpenSSL::SSL::SSLSocket#syswrite_nonblock -> Errno::ECONNRESET).
- Retrying the same file with the same token fails the same way every time. Over the past two weeks, every upload that hit ECONNRESET also failed on all 3 retries.
- After another request for the same account received "401 expired_access_token" and our code refreshed the token, the same files that had been failing uploaded successfully on the first attempt.
- Some uploads made with an expired token do receive the 401 correctly (the refresh and retry then work as expected). We could not tell from our logs what makes the difference (for example, file size).
What we observe when testing locally
We tried to reproduce this from a local machine (not AWS), using a token that had really expired (get_current_account returned "expired_access_token"):
- Bodies of 5 MB or less were sent completely and we received 401 expired_access_token.
- For bodies of 20 MB and 50 MB, the connection was closed after roughly 7–15 MB had been sent (about 1.2 seconds in). Even so, we still received 401 expired_access_token every time, because Ruby's Net::HTTP ignores EPIPE while writing and then reads the response.
- We could not reproduce ECONNRESET locally.
It looks like the server stops reading the body about 1 second after rejecting the request and then closes the connection. Depending on how that close reaches the client, the client gets either EPIPE (and can still read the 401) or ECONNRESET (and cannot).
Questions
- When the access token is expired or invalid, is it expected that content.dropboxapi.com closes the connection before the whole request body has been received?
- Does /2/files/upload support "Expect: 100-continue", so that the client can receive the 401 before sending the body?
- Is the recommended approach to always check expires_at and refresh the token before uploading, rather than depending on the 401 response?
As a workaround, we now refresh the token before sending the request if it has already expired, based on expires_at.
Thanks in advance.