Problem getting mailchip api working

Bug report: resource.rawRequest() fails with "Truncated result: headers line not terminated" against Mailchimp (HTTP/2 + gzip)

Summary

In a Retool React app, a backend function that calls a REST API resource via myResource.rawRequest({...}) fails on every request to the Mailchimp Marketing API with:

Truncated result: headers line not terminated

The identical HTTP requests succeed when run with curl from outside Retool. A different REST resource in the same app (Airtable) works fine through the same rawRequest() pattern, so the issue appears specific to the Mailchimp endpoint's response (HTTP/2, gzip, served behind istio-envoy). We suspect Retool's outbound HTTP client mis-parses that response.

We need to know the supported way to make this call succeed from a backend function (correct rawRequest usage, a resource setting, or an alternative such as fetch).

Environment

  • Retool React app (built with the Retool app builder/agent).
  • Backend function (TypeScript) in the app, calling a REST resource binding: mailchimp.rawRequest({ path, method, headers, body }).
  • Resource: REST API, Manual Queries.
    • Base URL: https://<YOUR_DC>.api.mailchimp.com/3.0/ (e.g. https://us19.api.mailchimp.com/3.0/)
    • Authentication: Basic Auth β€” Username: anystring, Password: <YOUR_MAILCHIMP_API_KEY>
  • Environment: production.

The failing call (exact)

The backend function makes calls like:

// PUT upsert a member
await mailchimp.rawRequest({
  path: `lists/<YOUR_LIST_ID>/members/<md5_lowercase_email>`,
  method: 'PUT',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  body: { email_address: 'x@example.com', status_if_new: 'subscribed', merge_fields: { FNAME: 'X', LNAME: 'Y' } },
})

Result: throws Truncated result: headers line not terminated. Same for POST .../tags, DELETE .../members/<hash>, and (we believe) even a simple GET ping β€” see minimal repro below.

Minimal reproduction (please fill in your own values)

Create a REST resource named mailchimp (Base URL + Basic Auth as above). Then add this backend function and run it:

// Backend function: reproMailchimp
export default async function reproMailchimp() {
  // Simplest possible Mailchimp call: GET /3.0/ping (no body)
  const res = await mailchimp.rawRequest({
    path: 'ping',
    method: 'GET',
    headers: { Accept: 'application/json' },
  })
  return res
}
  • Expected: { "health_status": "Everything's Chimpy!" }
  • Actual: throws Truncated result: headers line not terminated

Baseline that works (same request, via curl)

Run from any shell with the same credentials:

dc="<YOUR_DC>"               # e.g. us19
apikey="<YOUR_MAILCHIMP_API_KEY>"
curl -sS "https://${dc}.api.mailchimp.com/3.0/ping" --user "anystring:${apikey}"
# => {"health_status":"Everything's Chimpy!"}   (HTTP 200)

A full write also works via curl (so the API, key, and payloads are valid):

hash=$(printf 'x@example.com' | md5sum | cut -d' ' -f1)
curl -sS --user "anystring:${apikey}" -H "Content-Type: application/json" \
  -X PUT "https://${dc}.api.mailchimp.com/3.0/lists/<YOUR_LIST_ID>/members/${hash}" \
  -d '{"email_address":"x@example.com","status_if_new":"subscribed","merge_fields":{"FNAME":"X","LNAME":"Y"}}'
# => 200, member JSON

Likely-relevant detail: the response is HTTP/2 + gzip behind istio-envoy

Inspecting the response headers directly:

curl -sS -D - -o /dev/null --user "anystring:${apikey}" \
  "https://${dc}.api.mailchimp.com/3.0/ping"
HTTP/2 200
content-type: application/json; charset=utf-8
content-encoding: gzip
vary: Accept-Encoding
server: istio-envoy

"headers line not terminated" is a response-header parsing failure. Our working hypothesis is that Retool's rawRequest HTTP client mishandles this endpoint's HTTP/2 and/or gzip/chunked response from Mailchimp's istio-envoy edge.

What we have already ruled out

  • Wrong URL / scheme: Base URL is exactly https://<dc>.api.mailchimp.com/3.0/ (https, trailing slash). Confirmed.
  • Auth: Basic Auth with the API key is correct; with no auth Mailchimp returns a clean 401, not this error. (Bearer also authenticates.) The error is identical regardless, so it is not auth.
  • Merge fields / payload: The exact bodies succeed via curl (200/204), and the required merge fields exist on the audience.
  • Concurrency: We reduced the backend function to one request at a time (no parallel rawRequest calls). No change.
  • Headers: Adding Content-Type: application/json, Accept: application/json, and Accept-Encoding: identity did not change it.
  • A different REST resource (Airtable) in the same app works through the same rawRequest pattern β€” so rawRequest is not universally broken; this is specific to the Mailchimp endpoint's response.

Questions for Retool

  1. What is the supported way to make this call succeed from a React-app backend function β€” correct rawRequest options, a resource setting (e.g. force HTTP/1.1, disable compression), or an alternative API?
  2. Is global fetch() available/permitted inside a backend function (to bypass rawRequest)? If so, is that recommended?
  3. Is "Truncated result: headers line not terminated" a known issue with HTTP/2 + gzip (istio-envoy) upstreams, and is there a fix or workaround?

Appendix: actual failure output (one run, 64 contacts)

Every contact fails identically:

Β· Loading contacts… Loaded 64 contacts… Syncing…
βœ— a@example.org β€” error Β· Truncated result: headers line not terminated
βœ— b@example.org β€” error Β· Truncated result: headers line not terminated
… (all 64) …
Β· Completed with 64 errors.

and here is the bug report retool agent generated directly after some more back and forth:

Retool Support Ticket β€” rawRequest "headers line not terminated" against Mailchimp (HTTP/2 + istio-envoy)

Paste this into a Retool support ticket. Redact nothing further β€” no secrets are included below.
Replace <ORG> / <APP> / <RESOURCE> placeholders with your details before sending.


Summary

A Retool REST API resource pointed at the Mailchimp Marketing API
(https://us19.api.mailchimp.com/3.0/) fails on member write operations with an
uncatchable backend error:

Truncated result: headers line not terminated

The error aborts the entire backend function invocation β€” it cannot be caught with
try/catch β€” so a batch sync that processes many contacts reports a failure for every
contact once the first bad response arrives.

The same HTTP requests succeed via curl from outside Retool. Mailchimp serves
HTTP/2 + gzip behind istio-envoy. This appears to be a transport/response-parsing
bug in Retool's HTTP client for this origin, not an issue in our function code.

Environment

  • Org: <ORG>
  • App: <APP>
  • Resource: <RESOURCE> (REST API type), base URL https://us19.api.mailchimp.com/3.0/,
    HTTP Basic auth configured on the resource (username anystring, password = Mailchimp API key).
  • Calling context: a serverless/backend function invoked from the app via a generated hook.
  • Methods affected: both mailchimp.rawRequest(...) (buffered) and
    mailchimp.rawRequestStreamRaw(...) (streaming).

Impact

  • A Mailchimp audience sync that does ~2 calls per contact (PUT /members/{hash} upsert +
    POST /members/{hash}/tags) fails for the whole batch.
  • Because the error is uncatchable, per-contact error handling does not help β€” the invocation
    dies outright.

What works vs. what fails (observed via Retool's backend code executor, live resource)

Call Transport Result
GET /ping (Γ—10) buffered + streaming OK (10/10)
GET /lists/{id}/members (β‰ˆ11 KB gzipped) buffered + streaming OK
PUT /lists/{id}/members/{hash} with invalid body β†’ 400 (tiny response) buffered OK
DELETE /lists/{id}/members/{hash} (nonexistent) β†’ 404 buffered OK
PUT /lists/{id}/members/{hash} with VALID body β†’ 200 (full member object) buffered FAIL: "Truncated result: headers line not terminated"
same valid PUT streaming FAIL (identical error)
delete-permanent (204) / DELETE archive (204) on an existing member both FAIL (intermittent)

Notes:

  • GET /ping is rock-solid (10/10) during the same session, so the connection/session itself
    is healthy β€” the failure is specific to the member-write responses.
  • The failure is intermittent: an identical 204 shape (POST .../tags with {tags: []})
    succeeded ~16 times earlier in a session, then later 204s on member delete failed.
  • fetch() is not available in the backend executor, so there is no in-function workaround that
    bypasses Retool's HTTP client.

Minimal reproduction

Run this in the Retool backend code executor against the Mailchimp REST resource. The control
(GET /ping) succeeds; the valid member PUT fails with the reported error.

import crypto from 'crypto'

const LIST = '<LIST_ID>'
const EMAIL = 'retool-repro-delete-me@example.com'
const HASH = crypto.createHash('md5').update(EMAIL.toLowerCase()).digest('hex')

// CONTROL β€” succeeds
await mailchimp.rawRequest({
  path: 'ping',
  method: 'GET',
  headers: { Accept: 'application/json' },
})

// FAILS with: "Truncated result: headers line not terminated"
// (uncatchable β€” aborts the whole invocation; try/catch does not trap it)
await mailchimp.rawRequest({
  path: `lists/${LIST}/members/${HASH}`,
  method: 'PUT',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  body: {
    email_address: EMAIL,
    status_if_new: 'subscribed',
    merge_fields: { FNAME: 'Repro', LNAME: 'Test' },
  },
})

return 'reached end' // never reached when the PUT triggers the bug

The same PUT via mailchimp.rawRequestStreamRaw({ ... }) (draining the byte stream) fails
identically.

Questions for Retool support

  1. Why does Retool's HTTP client emit "Truncated result: headers line not terminated" for this
    origin's larger HTTP/2 responses, while curl parses them fine?
  2. Why is the error uncatchable (it aborts the whole backend invocation rather than rejecting
    the awaited call)? This makes graceful per-item error handling impossible.
  3. Is there a resource-level setting to force HTTP/1.1 / disable HTTP/2 or change response
    decompression handling for a REST resource? (This class of istio-envoy + HTTP/2 parsing issue
    is typically resolved at the transport layer.)
  4. Is rawRequestStreamRaw expected to share the same parser/transport path? It exhibits the
    identical failure.

What we already tried (no resolution)

  • Added Accept-Encoding: identity to force an uncompressed response β€” no change.
  • Switched rawRequest β†’ rawRequestStreamRaw (streaming transport) β€” same failure on the
    valid member PUT.
  • Confirmed fetch() is blocked in the backend executor, so no alternate client is available
    in-function.

Update: I've now isolated this definitively to Retool's REST resource HTTP client β€” the identical backend code works perfectly outside Retool.

TL;DR

I took the exact backend function that fails in Retool, ran it byte-identical under a local Node.js Express harness where the mailchimp resource global is a ~20-line shim over Node's native fetch (undici), and every operation succeeds β€” including the PUT member upsert returning a 200 with a large JSON body, which is exactly the case that fails inside Retool with Truncated result: headers line not terminated. Same endpoints, same Basic auth, same request bodies, same Mailchimp datacenter. The only variable that changed is the HTTP client. The bug is in Retool's REST resource response handling, not in request construction, auth, payloads, or Mailchimp's infrastructure.

The experiment

My backend function (syncMailchimpBatch.ts) calls mailchimp.rawRequest({ path, method, headers, body }). To rule out my own code, I built a local dev harness that imports that file unmodified and supplies the mailchimp global as this shim:

// Local stand-in for Retool's `mailchimp` REST resource.
// MAILCHIMP_API_KEY comes from the environment; datacenter is its suffix (e.g. "-us21").
;(globalThis as any).mailchimp = {
  async rawRequest({ path: p, method = 'GET', body }) {
    const dc = process.env.MAILCHIMP_API_KEY.split('-')[1]
    const url = `https://${dc}.api.mailchimp.com/3.0/${p.replace(/^\/+/, '')}`
    const res = await fetch(url, {
      method,
      headers: {
        Authorization: `Basic ${Buffer.from(`anystring:${process.env.MAILCHIMP_API_KEY}`).toString('base64')}`,
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: body != null ? JSON.stringify(body) : undefined,
    })
    const text = await res.text()
    if (!res.ok) throw new Error(`Mailchimp ${method} ${p} -> ${res.status}: ${text}`)
    return { data: text ? JSON.parse(text) : null }
  },
}

That's the entire integration. Nothing exotic: plain HTTPS, Basic auth with the API key as the password, JSON in/out. Node's built-in fetch (undici) handles Mailchimp's responses β€” served behind istio-envoy with HTTP/2 and gzip β€” without any issue.

Results with identical calling code

Operation Under Retool's mailchimp resource Under the undici shim
PUT lists/{LIST_ID}/members/{md5} (upsert, 200 + full member JSON) Fails β€” Truncated result: headers line not terminated Succeeds
POST lists/{LIST_ID}/members/{md5}/tags (tag reconcile) Fails β€” same error Succeeds
DELETE lists/{LIST_ID}/members/{md5} (archive) Fails intermittently β€” same error Succeeds (404/405 surface normally as catchable errors)
Error paths (400 invalid body, 404, 405, 429 retry) 4xx responses parse OK Succeed / catchable as expected

A full audience sync (upsert + tags per contact, processed strictly sequentially) runs end-to-end with zero failures under the shim.

What this rules in/out for your team

  • Not request construction: byte-identical caller code, headers, and bodies in both environments.
  • Not auth: same Basic anystring:<key> header in both; 200s are coming back from Mailchimp either way β€” Retool fails while parsing the successful response.
  • Not concurrency: I had earlier forced strictly sequential requests (CONCURRENCY = 1) inside Retool as a mitigation; single in-flight requests still fail.
  • Not Mailchimp: undici and curl both parse these exact responses fine.
  • The correlation that should narrow it down: failures track responses with substantial JSON bodies (a 200 member upsert returns the full member object); GET /ping and small 4xx error bodies parse fine. Combined with the wording of the error (headers line not terminated), this points at your REST resource's response parser mishandling Mailchimp's HTTP/2 + gzip (and/or chunked) responses β€” plausibly attempting an HTTP/1.x header parse on a stream it shouldn't.

Asks

  1. Fix the response parsing in the REST resource client for HTTP/2 + compressed responses β€” or expose a resource-level option to force HTTP/1.1 and/or disable transparent decompression so we can work around it.
  2. Make this error catchable in backend functions. Today it aborts the entire invocation, so a batch job can't even skip the failing item and continue β€” there is no graceful degradation path, and fetch() is blocked in the backend executor so there's no escape hatch.

Environment

  • Working harness: Node.js v24.15.0, native fetch (undici), macOS
  • Failing environment: Retool React app ("app with code") backend function, REST API resource, rawRequest (also reproduced with rawRequestStreamRaw)
  • Mailchimp Marketing API v3.0, key-suffix datacenter (https://<dc>.api.mailchimp.com/3.0/)

Happy to provide the full harness or run any diagnostic build you'd like against my Mailchimp account.

Thanks for the detailed report, @Peter_Kellner! I will dig into this and give you an update as soon as I can.