{"record":{"id":"4c11db670a970ef0","repo":"langgenius/dify","slug":"streaming-response-body-missing","errorCode":null,"errorMessage":"streaming response body missing","messagePattern":"streaming response body missing","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/api/app-run.ts","lineNumber":62,"sourceCode":"    // `http.stream` facade; stopTask / submitHumanInput are plain JSON and go through the\n    // generated oRPC contract. Both facades share this one transport.\n    this.orpc = createOpenApiClient(http)\n  }\n\n  async runStream(\n    appId: string,\n    body: Record<string, unknown>,\n    opts: StreamOptions = {},\n  ): Promise<AsyncIterable<SseEvent>> {\n    const res = await this.http.stream(`apps/${encodeURIComponent(appId)}:run`, {\n      method: 'POST',\n      json: body,\n      headers: { Accept: 'text/event-stream' },\n      signal: opts.signal,\n      throwOnError: true,\n      retryOnRateLimit: opts.retryOnRateLimit,\n    })\n    if (res.body === null) throw new Error('streaming response body missing')\n    return normalizeDifyStream(parseSSE(res.body, opts.signal))\n  }\n\n  async stopTask(appId: string, taskId: string): Promise<void> {\n    await this.orpc.apps.byAppId.tasks.byTaskId.stop.post({\n      params: { app_id: appId, task_id: taskId },\n    })\n  }\n\n  async submitHumanInput(\n    appId: string,\n    formToken: string,\n    action: string,\n    inputs: Record<string, unknown>,\n  ): Promise<void> {\n    await this.orpc.apps.byAppId.humanInputForms.byFormToken.submit.post({\n      params: { app_id: appId, form_token: formToken },\n      body: { action, inputs },","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/cli/src/api/app-run.ts#L44-L80","documentation":"Raised by DatasetsHitTestingBase._prepare_hit_testing_records when an individual element of the records list returned by HitTestingService.retrieve is not a dict. This is a defensive contract check that the retrieval service produced the expected record shape before response serialization. It is an internal/server-side failure, not caused by user input, and surfaces as a ValueError (HTTP 500 after the broad except in perform_hit_testing re-raises it).","triggerScenarios":"HitTestingService.retrieve returns a list whose element is None, a string, or a pydantic model that was not dumped to a dict. Typically the result of a custom retrieval backend, a malformed external knowledge adapter, or a regression in the retrieve service after a schema change.","commonSituations":"A plugin/external knowledge endpoint returns a non-object record; an in-development retrieval adapter forgets to call .model_dump() on segment objects; version skew between the retrieval service and the controller normalization layer.","solutions":["Inspect server logs for the full stack trace to identify which record and retrieval backend produced the non-dict value.","Ensure any custom/external retrieval implementation returns records as plain dicts (call model_dump(mode='json') on ORM/pydantic objects before returning).","Add a unit test against HitTestingService.retrieve output asserting every record is a dict with the expected keys."],"exampleFix":"# before (retrieval service)\nreturn {\"query\": q, \"records\": [some_segment_model, ...]}\n# after\nreturn {\"query\": q, \"records\": [s.model_dump(mode='json') if hasattr(s, 'model_dump') else s for s in segments]}","handlingStrategy":"try-catch","validationCode":"from typing import Any\n\ndef is_valid_records_payload(response: Any) -> bool:\n    return (\n        isinstance(response, dict)\n        and isinstance(response.get('records'), list)\n        and all(isinstance(r, dict) for r in response['records'])\n    )\n# in retrieval service: assert is_valid_records_payload(result) before returning","typeGuard":"from typing import Any\n\ndef is_hit_testing_record(value: Any) -> bool:\n    return isinstance(value, dict)","tryCatchPattern":"try:\n    return DatasetsHitTestingBase._prepare_hit_testing_records(response.get('records', []))\nexcept ValueError:\n    logger.exception('retrieve returned malformed records: %r', response.get('records'))\n    raise  # surfaces as 500; this is a server-side contract bug, not retryable","preventionTips":["Maintain a contract test asserting HitTestingService.retrieve always returns list[dict] records.","Call .model_dump(mode='json') on any pydantic/ORM segment before placing it in the records list.","Log the raw response shape when this ValueError fires so regressions are diagnosable."],"tags":["hit-testing","internal","validation","response-shape","retrieval","server-error"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}