paperclipai/paperclip · warning

reason must be a string up to 4000 characters

Error message

reason must be a string up to 4000 characters

What it means

Validation guard in the tool-gateway action-request decline route: the optional `reason` body field, when present, must be a string of at most 4000 characters. It fires when the decline reason is a non-string value or an over-long string, before the decline is recorded.

Source

Thrown at server/src/routes/tool-gateway.ts:621

          userId: req.actor.type === "board" ? req.actor.userId : null,
        },
      });
      res.json(actionRequest);
    } catch (err) {
      sendGatewayError(res, err);
    }
  });

  router.post("/tool-gateway/action-requests/:id/decline", async (req, res) => {
    try {
      assertBoard(req);
      const body = (req.body ?? {}) as { companyId?: string; reason?: string };
      const companyId = body.companyId ?? (typeof req.query.companyId === "string" ? req.query.companyId : null);
      if (!companyId) {
        res.status(400).json({ error: "companyId is required" });
        return;
      }
      if (body.reason !== undefined && (typeof body.reason !== "string" || body.reason.length > 4000)) { res.status(400).json({ error: "reason must be a string up to 4000 characters" }); return; }
      assertBoardMutationAccess(req, companyId);
      const actor = getActorInfo(req);
      const actionRequest = await toolGateway.declineActionRequest({
        companyId,
        actionRequestId: req.params.id,
        reason: body.reason,
        actor: {
          agentId: actor.agentId,
          userId: req.actor.type === "board" ? req.actor.userId : null,
        },
      });
      res.json(actionRequest);
    } catch (err) {
      sendGatewayError(res, err);
    }
  });

  router.get("/tool-gateway/runtime-slots", async (req, res) => {

View on GitHub (pinned to 01ad858492)

Solutions

  1. Send reason as a plain string of at most 4000 characters.
  2. Omit reason entirely if there is no explanation.
  3. Truncate client-side (e.g. reason.slice(0, 4000)) before sending.
  4. Coerce/validate the reason field type in the client before the request.

Example fix

// before
decline({ companyId, reason: longText })            // longText.length = 5200 -> 400
// after
decline({ companyId, reason: longText.slice(0, 4000) })
Defensive patterns

Strategy: validation

Validate before calling

if (reason !== undefined && (typeof reason !== 'string' || reason.length > 4000)) throw new TypeError('reason must be a string of at most 4000 characters');

Type guard

const isValidReason = (v) => v === undefined || (typeof v === 'string' && v.length <= 4000);

Try / catch

try {
  return await declineActionRequest({ actionRequestId, companyId, reason });
} catch (e) {
  if (e.status === 400 && /reason/.test(e.body?.error ?? '')) {
    return declineActionRequest({ actionRequestId, companyId, reason: String(reason ?? '').slice(0, 4000) });
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /api/tool-gateway/action-requests/:id/decline with { reason: 123 }, { reason: { text: 'x' } }, or a string longer than 4000 characters.

Common situations: User pastes a very long explanation into a decline dialog; client sends non-string reason after failed parsing; template renders 'null' or an object instead of a string.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/84791e244c7d9d36. Report an issue: GitHub.