Hi all,
I’m setting up an Uppy/Companion integration on a stand alone instance and have hit a wall with uploads from remote providers (e.g., OneDrive). My stack looks like this:
- App: https://dev.example.com Django, with upload endpoints like
https://dev.example.com/app/lib/<uuid>/upload/
- Uppy Companion: https://uploads.example.com (self-hosted via Docker)
- Proxy: Nginx in front of Django, with Cloudflare in front of both.
This is my companion configuration
box@companion-1:~/companion# cat docker-compose.yml
version: '3.8'
services:
uppy-companion:
image: transloadit/companion
container_name: uppy-companion
ports:
- 127.0.0.1:3020:3020
environment:
- NODE_ENV=dev
- COMPANION_PORT=3020
- COMPANION_HIDE_METRICS=false
- COMPANION_HIDE_WELCOME=false
- COMPANION_STREAMING_UPLOAD=true
- COMPANION_PROTOCOL=https
- COMPANION_DATADIR=/mnt/uppy-server-data
- COMPANION_DOMAIN=uploads.example.com
- COMPANION_SECRET=supersecretkey
- COMPANION_REDIS_URL=redis_url
- COMPANION_CLIENT_ORIGINS=https://example.com,https://dev.example.com
- COMPANION_ONEDRIVE_KEY=xxx
- COMPANION_ONEDRIVE_SECRET=xxx
volumes:
- /tmp/uppy-server-data:/mnt/uppy-server-data
restart: unless-stopped
These are the server logs I get from Companion:
uppy-companion | companion: 2025-05-30T08:11:26.591Z [debug] null Socket connection received. Starting remote download/upload.
uppy-companion | companion: 2025-05-30T08:11:26.632Z [debug] a96a79ab uploader.total.progress 20480 99140 20.66%
uppy-companion | ::ffff:192.168.176.1 - - [30/May/2025:08:11:36 +0000] "GET / HTTP/1.0" 200 1146 "-" "HCLB-HealthCheck"
uppy-companion | companion: 2025-05-30T08:11:41.696Z [error] upload.multipart.error RequestError: socket hang up
uppy-companion | at ClientRequest.<anonymous> (file:///app/node_modules/got/dist/source/core/index.js:792:107)
uppy-companion | at Object.onceWrapper (node:events:629:26)
uppy-companion | at ClientRequest.emit (node:events:526:35)
uppy-companion | at TLSSocket.socketOnEnd (node:_http_client:525:9)
uppy-companion | at TLSSocket.emit (node:events:526:35)
uppy-companion | at endReadableNT (node:internal/streams/readable:1359:12)
uppy-companion | at connResetException (node:internal/errors:720:14)
uppy-companion | at TLSSocket.socketOnEnd (node:_http_client:525:23)
uppy-companion | at TLSSocket.emit (node:events:526:35)
uppy-companion | at endReadableNT (node:internal/streams/readable:1359:12)
uppy-companion | at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
uppy-companion | input: undefined,
uppy-companion | code: 'ECONNRESET',
uppy-companion | timings: {
uppy-companion | start: 1748592686680,
uppy-companion | socket: 1748592686681,
uppy-companion | lookup: 1748592686682,
uppy-companion | connect: 1748592686688,
uppy-companion | secureConnect: 1748592686699,
uppy-companion | upload: undefined,
uppy-companion | response: undefined,
uppy-companion | end: undefined,
uppy-companion | error: 1748592701695,
uppy-companion | abort: undefined,
uppy-companion | phases: {
uppy-companion | wait: 1,
uppy-companion | dns: 1,
uppy-companion | tcp: 6,
uppy-companion | tls: 11,
uppy-companion | request: undefined,
uppy-companion | firstByte: undefined,
uppy-companion | download: undefined,
uppy-companion | total: 15015
uppy-companion | }
uppy-companion | },
uppy-companion | options: {
uppy-companion | request: undefined,
uppy-companion | agent: { http: undefined, https: undefined, http2: undefined },
uppy-companion | h2session: undefined,
uppy-companion | decompress: true,
uppy-companion | timeout: {
uppy-companion | connect: undefined,
uppy-companion | lookup: undefined,
uppy-companion | read: undefined,
uppy-companion | request: undefined,
uppy-companion | response: undefined,
uppy-companion | secureConnect: undefined,
uppy-companion | send: undefined,
uppy-companion | socket: undefined
uppy-companion | },
uppy-companion | prefixUrl: '',
uppy-companion | body: Object [AsyncGenerator] {},
uppy-companion | form: undefined,
uppy-companion | json: undefined,
uppy-companion | cookieJar: undefined,
uppy-companion | ignoreInvalidCookies: false,
uppy-companion | searchParams: undefined,
uppy-companion | dnsLookup: undefined,
uppy-companion | dnsCache: undefined,
uppy-companion | context: {},
uppy-companion | hooks: {
uppy-companion | init: [],
uppy-companion | beforeRequest: [],
uppy-companion | beforeError: [],
uppy-companion | beforeRedirect: [],
uppy-companion | beforeRetry: [],
uppy-companion | afterResponse: []
uppy-companion | },
uppy-companion | followRedirect: true,
uppy-companion | maxRedirects: 10,
uppy-companion | cache: undefined,
uppy-companion | throwHttpErrors: true,
uppy-companion | username: '',
uppy-companion | password: '',
uppy-companion | http2: false,
uppy-companion | allowGetBody: false,
uppy-companion | headers: {
uppy-companion | 'user-agent': 'got (https://github.com/sindresorhus/got)',
uppy-companion | 'x-requested-with': 'XMLHttpRequest',
uppy-companion | 'content-type': 'multipart/form-data; boundary=form-data-boundary-oyye7getersmxw3t',
uppy-companion | 'accept-encoding': 'gzip, deflate, br'
uppy-companion | },
uppy-companion | methodRewriting: false,
uppy-companion | dnsLookupIpVersion: undefined,
uppy-companion | parseJson: [Function: parse],
uppy-companion | stringifyJson: [Function: stringify],
uppy-companion | retry: {
uppy-companion | limit: 2,
uppy-companion | methods: [ 'GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'TRACE' ],
uppy-companion | statusCodes: [
uppy-companion | 408, 413, 429, 500,
uppy-companion | 502, 503, 504, 521,
uppy-companion | 522, 524
uppy-companion | ],
uppy-companion | errorCodes: [
uppy-companion | 'ETIMEDOUT',
uppy-companion | 'ECONNRESET',
uppy-companion | 'EADDRINUSE',
uppy-companion | 'ECONNREFUSED',
uppy-companion | 'EPIPE',
uppy-companion | 'ENOTFOUND',
uppy-companion | 'ENETUNREACH',
uppy-companion | 'EAI_AGAIN'
uppy-companion | ],
uppy-companion | maxRetryAfter: undefined,
uppy-companion | calculateDelay: [Function: calculateDelay],
uppy-companion | backoffLimit: Infinity,
uppy-companion | noise: 100
uppy-companion | },
uppy-companion | localAddress: undefined,
uppy-companion | method: 'POST',
uppy-companion | createConnection: undefined,
uppy-companion | cacheOptions: {
uppy-companion | shared: undefined,
uppy-companion | cacheHeuristic: undefined,
uppy-companion | immutableMinTimeToLive: undefined,
uppy-companion | ignoreCargoCult: undefined
uppy-companion | },
uppy-companion | https: {
uppy-companion | alpnProtocols: undefined,
uppy-companion | rejectUnauthorized: undefined,
uppy-companion | checkServerIdentity: undefined,
uppy-companion | certificateAuthority: undefined,
uppy-companion | key: undefined,
uppy-companion | certificate: undefined,
uppy-companion | passphrase: undefined,
uppy-companion | pfx: undefined,
uppy-companion | ciphers: undefined,
uppy-companion | honorCipherOrder: undefined,
uppy-companion | minVersion: undefined,
uppy-companion | maxVersion: undefined,
uppy-companion | signatureAlgorithms: undefined,
uppy-companion | tlsSessionLifetime: undefined,
uppy-companion | dhparam: undefined,
uppy-companion | ecdhCurve: undefined,
uppy-companion | certificateRevocationLists: undefined
uppy-companion | },
uppy-companion | encoding: undefined,
uppy-companion | resolveBodyOnly: false,
uppy-companion | isStream: false,
uppy-companion | responseType: 'text',
uppy-companion | url: URL {
uppy-companion | href: 'https://dev.example.com/app/lib/2df97b60-46eb-4139-bba6-52b2c4efb8a6/upload/',
uppy-companion | origin: 'https://dev.example.com',
uppy-companion | protocol: 'https:',
uppy-companion | username: '',
uppy-companion | password: '',
uppy-companion | host: 'dev.example.com',
uppy-companion | hostname: 'dev.example.com',
uppy-companion | port: '',
uppy-companion | pathname: '/app/lib/2df97b60-46eb-4139-bba6-52b2c4efb8a6/upload/',
uppy-companion | search: '',
uppy-companion | searchParams: URLSearchParams {},
uppy-companion | hash: ''
uppy-companion | },
uppy-companion | pagination: {
uppy-companion | transform: [Function: transform],
uppy-companion | paginate: [Function: paginate],
uppy-companion | filter: [Function: filter],
uppy-companion | shouldContinue: [Function: shouldContinue],
uppy-companion | countLimit: Infinity,
uppy-companion | backoff: 0,
uppy-companion | requestLimit: 10000,
uppy-companion | stackAllItems: false
uppy-companion | },
uppy-companion | setHost: true,
uppy-companion | maxHeaderSize: undefined,
uppy-companion | signal: undefined,
uppy-companion | enableUnixSockets: false
uppy-companion | }
uppy-companion | }
uppy-companion | companion: 2025-05-30T08:11:41.699Z [debug] a96a79ab cleanup
uppy-companion | companion: 2025-05-30T08:11:41.700Z [error] a96a79ab uploader.error Error
uppy-companion | at #uploadMultipart (/app/lib/server/Uploader.js:567:37)
uppy-companion | at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
uppy-companion | at async Uploader.uploadStream (/app/lib/server/Uploader.js:252:40)
uppy-companion | at async Uploader.tryUploadStream (/app/lib/server/Uploader.js:279:25)
uppy-companion | at async /app/lib/server/helpers/upload.js:28:9 {
uppy-companion | extraData: {
uppy-companion | responseText: undefined,
uppy-companion | status: 302,
uppy-companion | statusText: 'Moved Temporarily',
uppy-companion | headers: {
uppy-companion | date: 'Fri, 30 May 2025 08:11:26 GMT',
uppy-companion | 'content-type': 'text/html',
uppy-companion | 'transfer-encoding': 'chunked',
uppy-companion | connection: 'close',
uppy-companion | location: 'https://dev.example.com/app/lib/2df97b60-46eb-4139-bba6-52b2c4efb8a6/upload/',
uppy-companion | 'cf-cache-status': 'DYNAMIC',
uppy-companion | 'report-to': '{"endpoints":[{"url":"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=ffRZqqPo%2Bz79XJnQsjS7s44hABnUEH43Q%2B%2BmaCErhGFQ%2FQrgox%2FZZFkZ1T%2F8bI23Pf67ijUlGlS1iEKhZz%2FOVJ1Vzglt%2BiCaBm4OMngpRp%2FSpLht2uEGv2CdleRhxrTg9LT6AA%3D%3D"}],"group":"cf-nel","max_age":604800}',
uppy-companion | nel: '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}',
uppy-companion | server: 'cloudflare',
uppy-companion | 'cf-ray': '947cc2c37f339750-FRA',
uppy-companion | 'server-timing': 'cfL4;desc="?proto=TCP&rtt=5863&min_rtt=5863&rtt_var=2931&sent=20&recv=81&lost=0&retrans=0&sent_bytes=0&recv_bytes=99988&delivery_rate=0&cwnd=249&unsent_bytes=0&cid=0000000000000000&ts=0&x=0"'
uppy-companion | }
uppy-companion | }
uppy-companion | }
The Problem
When Uppy (via Companion) tries to upload a file (e.g., after fetching from OneDrive), the Companion server logs show a 302 redirect in response to its POST request to https://dev.example.com/app/lib/<uuid>/upload/
. Example nginx log:
POST /app/lib/2df97b60-46eb-4139-bba6-52b2c4efb8a6/upload/ HTTP/1.1" 302 154 "-" "got (https://github.com/sindresorhus/got)"
However, there is no follow-up request after this redirect – it seems Companion does not follow the 302, so the upload fails. Moreover, HTTP/1.1 protocol in the above line suggests that the request was not done by HTTPS, so I am presuming that there is a redirect 302 to the HTTPS, even if the COMPANION_PROTOCOL=https on uploads.example.com
.
Moreover, the request does not appear in django app logs.
What I’ve Tried & Discovered
- CSRF/Auth: Disabled all authentication and CSRF checks to make the endpoint completely public for testing – still get the 302.
- nginx: Confirmed that the upload endpoint works with curl and via browser POSTs.
- Cloudflare: Suspect that Cloudflare (or possibly nginx) is enforcing HTTPS and sending the 302, e.g. HTTP → HTTPS. But Companion should be using HTTPS URLs.
- Companion config: Set
COMPANION_PROTOCOL=https
,COMPANION_DOMAIN=uploads.example.com
, and correctCOMPANION_CLIENT_ORIGINS
. - curl debug: Using verbose curl, I only see the 302 in the response – no Location header in the nginx logs.
- Logs: The request never hits Django’s view, so the 302 is before application level (must be nginx or Cloudflare).
My Questions
-
Does Uppy Companion follow HTTP redirects when uploading to a target endpoint?
- The logs suggest it doesn’t, and the upload simply fails if it gets a 302. But the companion logs show a
followedirect: true,
so I’m suspecting it should folllow the 302.
- The logs suggest it doesn’t, and the upload simply fails if it gets a 302. But the companion logs show a
-
Is there a way to force Companion to use HTTPS for the upload endpoint, or ensure it never tries HTTP?
- All my configs use HTTPS, but I suspect Companion or its internal logic is hitting HTTP, which then triggers the redirect.
-
Is this a known limitation? Should remote uploads always work only if there are no redirects of any kind (including HTTP → HTTPS)?
-
How do you recommend handling remote provider uploads behind proxies/CDNs that may enforce HTTPS? Unfortunately I cannot send everything to S3 because I need to download the files on companion server and send it back to django app to perform some tasks before sending to a S3 bucket.
-
Is there a way to debug or configure Companion so that it can cope with such infrastructure, or at least see better error messages?
What Would Help
- Documentation pointers: Anything on how Companion handles redirects, or best practices for endpoint config.
- Real-world advice: How do you handle remote uploads with Companion when sitting behind Cloudflare/nginx enforcing HTTPS?
Thanks in advance for any help!