paperclipai/paperclip · error · RailwayError
railway_invalid_response
railway_invalid_response
Error message
Railway returned an empty response.
What it means
RailwayError (code railway_invalid_response) thrown by boundedResponseText when the HTTP Response has no readable body stream (response.body?.getReader() is undefined). The library needs a streaming body to read and bound Railway API responses; without one it cannot proceed, so it throws this client-side error (no HTTP status appended).
Solutions
- Check the response.status first and handle 204/empty bodies before calling body()/boundedResponseText
- Ensure the response is read exactly once — do not call response.json()/text() before boundedResponseText
- Use Node >= 18 with native fetch (undici) so response.body exposes getReader()
- In tests/mocks, stub fetch with a real ReadableStream body instead of a bare Response-like object
Example fix
// before
const text = await boundedResponseText(res, signal);
// after
if (res.status === 204 || !res.body) {
return ""; // nothing to read
}
const text = await boundedResponseText(res, signal); Defensive patterns
Strategy: type-guard
Validate before calling
function hasReadableBody(res) {
return res != null && typeof res.body?.getReader === "function";
}
if (res.status === 204 || !hasReadableBody(res)) return ""; // empty is acceptable Type guard
function isReadableResponse(res) {
return typeof res?.body?.getReader === "function";
} Try / catch
try {
const text = await boundedResponseText(res, signal);
} catch (e) {
if (e.code === "railway_invalid_response" && !res.body) {
logger.warn({ status: res.status }, "Railway returned no body; treating as empty");
return "";
}
throw e;
} Prevention
- Handle 204/HEAD/empty-body responses before invoking the body reader
- Read the response exactly once — never consume body via .json()/.text() first
- Require Node >= 18 native fetch so response.body streams are available
- In tests, mock fetch with a real Response containing a ReadableStream body
When it happens
Trigger: Calling a Railway API wrapper whose fetch returned a response with a null body — e.g. a 204 No Content, a HEAD request, a response already consumed by another reader, or a runtime/polyfill (older undici, service-worker intercepted fetch) that does not expose body streams.
Common situations: Hitting an endpoint that legitimately returns an empty body (204) through the generic body() helper; a proxy/gateway stripping the body; double-reading the response (body already consumed elsewhere); Node < 18 or polyfilled fetch without streaming support; tests stubbing fetch with { ok: true } and no body.
Related errors
- returned an unreadable inventory response
- railway_output_limit
- Announcement request failed
- Anthropic Managed Agents request failed with HTTP
- Artifact download failed: HTTP
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/0d6cbd8095745bf9.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/railway.ts:148
services: `query PaperclipRailwayServices($projectId:String!,$first:Int!,$after:String) { project(id:$projectId) { id services(first:$first,after:$after) { edges { node { id name projectId } } ${pageInfo} } } }`,
environments: `query PaperclipRailwayEnvironments($projectId:String!,$first:Int!,$after:String) { project(id:$projectId) { id environments(first:$first,after:$after) { edges { node { id name projectId } } ${pageInfo} } } }`,
target: `query PaperclipRailwayTarget($projectId:String!,$environmentId:String!,$serviceId:String!) { project(id:$projectId) { id } environment(id:$environmentId) { id projectId } service(id:$serviceId) { id projectId } serviceInstance(environmentId:$environmentId,serviceId:$serviceId) { ${instanceFields} } }`,
deployment: `query PaperclipRailwayDeployment($deploymentId:String!) { deployment(id:$deploymentId) { ${deploymentFields} instances { id } } }`,
deployments: `query PaperclipRailwayDeployments($input:DeploymentListInput!,$first:Int!,$after:String) { deployments(input:$input,first:$first,after:$after) { edges { node { ${deploymentFields} } } ${pageInfo} } }`,
buildLogs: `query PaperclipRailwayBuildLogs($deploymentId:String!,$limit:Int!,$startDate:DateTime,$endDate:DateTime,$filter:String) { buildLogs(deploymentId:$deploymentId,limit:$limit,startDate:$startDate,endDate:$endDate,filter:$filter) { timestamp message severity } }`,
runtimeLogs: `query PaperclipRailwayRuntimeLogs($deploymentId:String!,$limit:Int!,$startDate:DateTime,$endDate:DateTime,$filter:String) { deploymentLogs(deploymentId:$deploymentId,limit:$limit,startDate:$startDate,endDate:$endDate,filter:$filter) { timestamp message severity } }`,
redeploy: `mutation PaperclipRailwayRedeploy($deploymentId:String!) { deploymentRedeploy(id:$deploymentId,usePreviousImageTag:true) { id status } }`,
restart: `mutation PaperclipRailwayRestart($deploymentId:String!) { deploymentRestart(id:$deploymentId) }`,
rollback: `mutation PaperclipRailwayRollback($deploymentId:String!) { deploymentRollback(id:$deploymentId) }`,
};
function record(value: unknown): Record<string, any> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {};
}
async function boundedResponseText(response: Response, signal: AbortSignal): Promise<string> {
const reader = response.body?.getReader();
if (!reader) throw new RailwayError("railway_invalid_response", "Railway returned an empty response.");
const chunks: Uint8Array[] = [];
let size = 0;
try {
for (;;) {
signal.throwIfAborted();
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > 1024 * 1024) throw new RailwayError("railway_output_limit", "Railway's response exceeded the limit. Request fewer results or a shorter log interval.");
chunks.push(value);
}
} finally { await reader.cancel().catch(() => {}); }
return Buffer.concat(chunks).toString("utf8");
}
/** Discover a consented workspace without requesting account-wide API access. */
export async function discoverRailwayWorkspace(options: RailwayClientOptions): Promise<string> {
const send = (init: RequestInit) => options.request(RAILWAY_MCP_URL, { ...init, redirect: "error", signal: options.signal });View on GitHub (pinned to 3f1d897a7c)