{"record":{"id":"e6bfd6dd552caeaa","repo":"can1357/oh-my-pi","slug":"devin-connect-frame-length-len-exceeds-max-co","errorCode":null,"errorMessage":"Devin Connect frame length ${len} exceeds ${MAX_CONNECT_FRAME_PAYLOAD}-byte cap","messagePattern":"Devin Connect frame length (.+?) exceeds (.+?)-byte cap","errorType":"exception","errorClass":"AIError.ProviderResponseError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/devin.ts","lineNumber":247,"sourceCode":"\t\t\tconst reader = body.getReader();\n\t\t\tlet pending = Buffer.alloc(0);\n\n\t\t\tfor (;;) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (value && value.length > 0) {\n\t\t\t\t\t// Steady state drains fully per chunk; view the fresh reader chunk\n\t\t\t\t\t// instead of copying it through Buffer.concat (see aws-eventstream.ts).\n\t\t\t\t\tpending =\n\t\t\t\t\t\tpending.length === 0\n\t\t\t\t\t\t\t? Buffer.from(value.buffer, value.byteOffset, value.byteLength)\n\t\t\t\t\t\t\t: Buffer.concat([pending, value]);\n\t\t\t\t}\n\n\t\t\t\twhile (pending.length >= 5) {\n\t\t\t\t\tconst flag = pending[0];\n\t\t\t\t\tconst len = pending.readUInt32BE(1);\n\t\t\t\t\tif (len > MAX_CONNECT_FRAME_PAYLOAD) {\n\t\t\t\t\t\tthrow new AIError.ProviderResponseError(\n\t\t\t\t\t\t\t`Devin Connect frame length ${len} exceeds ${MAX_CONNECT_FRAME_PAYLOAD}-byte cap`,\n\t\t\t\t\t\t\t{ provider: model.provider, kind: \"envelope\" },\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (pending.length < 5 + len) break;\n\t\t\t\t\tconst payload = pending.subarray(5, 5 + len);\n\t\t\t\t\tpending = pending.subarray(5 + len);\n\n\t\t\t\t\tif (flag & CONNECT_END_STREAM_FLAG) {\n\t\t\t\t\t\tconst trailerBytes = flag & CONNECT_COMPRESSED_FLAG ? gunzipSync(payload) : payload;\n\t\t\t\t\t\tconst trailerError = readConnectTrailerError(trailerBytes.toString(\"utf8\").trim());\n\t\t\t\t\t\tif (trailerError) {\n\t\t\t\t\t\t\t// #4218: these rejections carry no HTTP error body, so the raw\n\t\t\t\t\t\t\t// trailer is the only server-side evidence. Log it with the\n\t\t\t\t\t\t\t// request shape before classification discards it.\n\t\t\t\t\t\t\tlogger.warn(\"devin: stream rejected via Connect trailer\", {\n\t\t\t\t\t\t\t\tmodel: model.id,\n\t\t\t\t\t\t\t\tcode: trailerError.code,","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/devin.ts#L229-L265","documentation":"This ProviderResponseError is thrown while parsing the Devin Connect binary stream in streamDevin. The wire protocol prefixes each frame with a 1-byte flag and a 4-byte big-endian length; before reading a frame body the parser checks the declared length against MAX_CONNECT_FRAME_PAYLOAD. A length over the cap means the byte stream is no longer a valid Connect framing sequence (desync or a non-Connect response), so continuing would allocate unbounded memory or misparse everything downstream.","triggerScenarios":"A streamed response from the Devin endpoint whose first five bytes decode to a frame length larger than MAX_CONNECT_FRAME_PAYLOAD — e.g. the server returned an HTML error page, JSON, or compressed/unframed data that the parser misread as a length header, or protocol/version drift changed the framing.","commonSituations":"Devin gateway changes or proxies returning HTML/JSON error bodies over HTTP 200; base URL pointing at a non-Connect endpoint; Devin server-side protocol changes; corrupt intermediate proxy mangling the byte stream.","solutions":["Verify the Devin base URL points at the Connect streaming endpoint, not a plain HTTP/JSON route","Retry once — transient proxy corruption can desync framing; check whether the error recurs on every request","Capture the model/provider config and update the pi-ai package if Devin changed its Connect framing protocol","Check any corporate proxy/TLS-inspecting middlebox for body rewriting"],"exampleFix":"// before: pointing baseUrl at a JSON REST endpoint\nbaseUrl: \"https://api.devin.ai/v1\"\n// after: use the Connect streaming endpoint for streamDevin\nbaseUrl: \"https://connect.devin.ai\" // per Devin Connect docs","handlingStrategy":"validation","validationCode":"// Confirm the endpoint speaks Connect framing before streaming:\nconst res = await fetch(baseUrl + \"/devin.Connect/Stream\", { method: \"POST\" });\nconst ct = res.headers.get(\"content-type\") ?? \"\";\nif (!ct.includes(\"connect\") && !ct.includes(\"proto\")) {\n  throw new Error(`Expected Connect stream, got content-type ${ct}`);\n}","typeGuard":"function isConnectFrameHeader(buf: Uint8Array): boolean {\n  if (buf.length < 5) return false;\n  const len = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(1, false);\n  return len > 0 && len <= 4 * 1024 * 1024; // within sane frame cap\n}","tryCatchPattern":"try {\n  await streamDevin(model, context, options);\n} catch (err) {\n  if (err instanceof AIError.ProviderResponseError && err.message.includes(\"-byte cap\")) {\n    // framing desync: surface the provider config and retry once or report a protocol bug\n    logger.error(\"Devin Connect framing desync\", { model: model.id, cause: err });\n  } else throw err;\n}","preventionTips":["Point baseUrl strictly at the Connect streaming endpoint, never a JSON/HTML route","Keep pi-ai updated when Devin changes protocol versions","Watch for proxies/TLS-inspection middleboxes that rewrite response bodies","Log first bytes of unexpected failures to diagnose desync early"],"tags":["protocol","streaming","provider-response","devin"],"backgroundTag":"framing-protocol-desync","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}