Skip to content

Commit a7d6a61

Browse files
authored
fix(api): declare mount binary responses, regenerate the Fern client (#5518)
The three mount routes that return raw bytes declared no response media type, so FastAPI advertised application/json for them. The generated clients then parsed a zip or a file body as JSON and corrupted it, which is why the frontend had to call these routes through raw axios. Declare the real media types (application/zip for the archive export, application/octet-stream for the two file downloads). Setting responses= alone is not enough: FastAPI keeps its default application/json entry unless the route also declares a response_class with no media type, so both are set. Runtime behavior is unchanged, since each handler already returns its own Response. Regenerate the TypeScript client once from the resulting spec, replacing the exportMountFiles method that was hand-written into the generated directory after the /files/export rename. The three methods now return core.BinaryResponse with responseType binary-response, which exposes both .blob() and .stream(). The regeneration also corrects two pieces of drift the hand-edits had left behind: GetMountFilesRequest.order becomes a generated enum, and MountFile moves under the mounts resource. Refs #5417 Claude-Session: https://claude.ai/code/session_011npNAXwcM2adqdSX6QGcsz
1 parent 894c23b commit a7d6a61

5 files changed

Lines changed: 61 additions & 35 deletions

File tree

api/oss/src/apis/fastapi/mounts/router.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
from typing import Literal, Optional
33
from uuid import UUID
44

5-
from fastapi import APIRouter, HTTPException, Query, Request, UploadFile, status
5+
from fastapi import (
6+
APIRouter,
7+
HTTPException,
8+
Query,
9+
Request,
10+
Response,
11+
UploadFile,
12+
status,
13+
)
14+
from fastapi.responses import StreamingResponse
615

716
from oss.src.utils.exceptions import intercept_exceptions
817

@@ -42,6 +51,8 @@
4251
MountsResponse,
4352
)
4453
from oss.src.apis.fastapi.mounts.utils import (
54+
BINARY_RESPONSE,
55+
ZIP_RESPONSE,
4556
download_mount_file,
4657
merge_mount_query,
4758
sign_mount_credentials,
@@ -200,6 +211,8 @@ def __init__(
200211
methods=["POST"],
201212
operation_id="export_mount_files",
202213
response_model=None,
214+
response_class=StreamingResponse,
215+
responses=ZIP_RESPONSE,
203216
status_code=status.HTTP_200_OK,
204217
)
205218
self.router.add_api_route(
@@ -247,6 +260,8 @@ def __init__(
247260
methods=["GET"],
248261
operation_id="download_mount_file",
249262
response_model=None,
263+
response_class=Response,
264+
responses=BINARY_RESPONSE,
250265
status_code=status.HTTP_200_OK,
251266
)
252267
self.router.add_api_route(

api/oss/src/apis/fastapi/mounts/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@
2121
# Regular-file mode for archive members (owner rw, group/other r).
2222
_ARCHIVE_FILE_MODE = S_IFREG | 0o644
2323

24+
# OpenAPI declarations for the routes below that return raw bytes rather than JSON. Without them
25+
# FastAPI advertises `application/json`, and the generated clients then parse the body as JSON and
26+
# corrupt the payload. A route must also declare a `response_class` with no media type, or FastAPI
27+
# keeps its default `application/json` entry alongside these.
28+
ZIP_RESPONSE = {
29+
200: {
30+
"content": {
31+
"application/zip": {"schema": {"type": "string", "format": "binary"}}
32+
}
33+
}
34+
}
35+
BINARY_RESPONSE = {
36+
200: {
37+
"content": {
38+
"application/octet-stream": {
39+
"schema": {"type": "string", "format": "binary"}
40+
}
41+
}
42+
}
43+
}
44+
2445

2546
def _content_disposition_attachment(filename: str) -> str:
2647
"""Build a safe `Content-Disposition: attachment` header value (RFC 6266).

api/oss/src/apis/fastapi/sessions/router.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@
2020
from functools import wraps
2121
from uuid import UUID
2222

23-
from fastapi import APIRouter, HTTPException, Query, Request, UploadFile, status
23+
from fastapi import (
24+
APIRouter,
25+
HTTPException,
26+
Query,
27+
Request,
28+
Response,
29+
UploadFile,
30+
status,
31+
)
2432
from fastapi.responses import JSONResponse
2533
from typing import Any, Optional, Union
2634

@@ -71,6 +79,7 @@
7179
from oss.src.core.mounts.service import MountsService
7280
from oss.src.apis.fastapi.mounts.router import handle_mount_exceptions
7381
from oss.src.apis.fastapi.mounts.utils import (
82+
BINARY_RESPONSE,
7483
download_mount_file,
7584
sign_mount_credentials,
7685
upload_mount_file,
@@ -919,6 +928,8 @@ def __init__(
919928
methods=["GET"],
920929
operation_id="download_session_mount_file",
921930
response_model=None,
931+
response_class=Response,
932+
responses=BINARY_RESPONSE,
922933
status_code=status.HTTP_200_OK,
923934
)
924935

web/packages/agenta-api-client/src/generated/api/resources/mounts/client/Client.ts

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -527,32 +527,26 @@ export class MountsClient {
527527
}
528528

529529
/**
530-
* @param {AgentaApi.MountArchiveRequest} request
531-
* @param {MountsClient.RequestOptions} requestOptions - Request-specific configuration.
532-
*
533530
* @throws {@link AgentaApi.UnprocessableEntityError}
534-
*
535-
* @example
536-
* await client.mounts.exportMountFiles()
537531
*/
538532
public exportMountFiles(
539533
request: AgentaApi.MountArchiveRequest = {},
540534
requestOptions?: MountsClient.RequestOptions,
541-
): core.HttpResponsePromise<unknown> {
535+
): core.HttpResponsePromise<core.BinaryResponse> {
542536
return core.HttpResponsePromise.fromPromise(this.__exportMountFiles(request, requestOptions));
543537
}
544538

545539
private async __exportMountFiles(
546540
request: AgentaApi.MountArchiveRequest = {},
547541
requestOptions?: MountsClient.RequestOptions,
548-
): Promise<core.WithRawResponse<unknown>> {
542+
): Promise<core.WithRawResponse<core.BinaryResponse>> {
549543
const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest();
550544
const _headers: core.Fetcher.Args["headers"] = mergeHeaders(
551545
_authRequest.headers,
552546
this._options?.headers,
553547
requestOptions?.headers,
554548
);
555-
const _response = await core.fetcher({
549+
const _response = await core.fetcher<core.BinaryResponse>({
556550
url: core.url.join(
557551
(await core.Supplier.get(this._options.baseUrl)) ??
558552
(await core.Supplier.get(this._options.environment)) ??
@@ -565,6 +559,7 @@ export class MountsClient {
565559
queryParameters: requestOptions?.queryParams,
566560
requestType: "json",
567561
body: request,
562+
responseType: "binary-response",
568563
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
569564
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
570565
withCredentials: true,
@@ -897,28 +892,19 @@ export class MountsClient {
897892
}
898893

899894
/**
900-
* @param {AgentaApi.DownloadMountFileRequest} request
901-
* @param {MountsClient.RequestOptions} requestOptions - Request-specific configuration.
902-
*
903895
* @throws {@link AgentaApi.UnprocessableEntityError}
904-
*
905-
* @example
906-
* await client.mounts.downloadMountFile({
907-
* mount_id: "mount_id",
908-
* path: "path"
909-
* })
910896
*/
911897
public downloadMountFile(
912898
request: AgentaApi.DownloadMountFileRequest,
913899
requestOptions?: MountsClient.RequestOptions,
914-
): core.HttpResponsePromise<unknown> {
900+
): core.HttpResponsePromise<core.BinaryResponse> {
915901
return core.HttpResponsePromise.fromPromise(this.__downloadMountFile(request, requestOptions));
916902
}
917903

918904
private async __downloadMountFile(
919905
request: AgentaApi.DownloadMountFileRequest,
920906
requestOptions?: MountsClient.RequestOptions,
921-
): Promise<core.WithRawResponse<unknown>> {
907+
): Promise<core.WithRawResponse<core.BinaryResponse>> {
922908
const { mount_id: mountId, path } = request;
923909
const _queryParams: Record<string, unknown> = {
924910
path,
@@ -929,7 +915,7 @@ export class MountsClient {
929915
this._options?.headers,
930916
requestOptions?.headers,
931917
);
932-
const _response = await core.fetcher({
918+
const _response = await core.fetcher<core.BinaryResponse>({
933919
url: core.url.join(
934920
(await core.Supplier.get(this._options.baseUrl)) ??
935921
(await core.Supplier.get(this._options.environment)) ??
@@ -939,6 +925,7 @@ export class MountsClient {
939925
method: "GET",
940926
headers: _headers,
941927
queryParameters: { ..._queryParams, ...requestOptions?.queryParams },
928+
responseType: "binary-response",
942929
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
943930
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
944931
withCredentials: true,

web/packages/agenta-api-client/src/generated/api/resources/sessions/client/Client.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,28 +1289,19 @@ export class SessionsClient {
12891289
}
12901290

12911291
/**
1292-
* @param {AgentaApi.DownloadSessionMountFileRequest} request
1293-
* @param {SessionsClient.RequestOptions} requestOptions - Request-specific configuration.
1294-
*
12951292
* @throws {@link AgentaApi.UnprocessableEntityError}
1296-
*
1297-
* @example
1298-
* await client.sessions.downloadSessionMountFile({
1299-
* mount_id: "mount_id",
1300-
* path: "path"
1301-
* })
13021293
*/
13031294
public downloadSessionMountFile(
13041295
request: AgentaApi.DownloadSessionMountFileRequest,
13051296
requestOptions?: SessionsClient.RequestOptions,
1306-
): core.HttpResponsePromise<unknown> {
1297+
): core.HttpResponsePromise<core.BinaryResponse> {
13071298
return core.HttpResponsePromise.fromPromise(this.__downloadSessionMountFile(request, requestOptions));
13081299
}
13091300

13101301
private async __downloadSessionMountFile(
13111302
request: AgentaApi.DownloadSessionMountFileRequest,
13121303
requestOptions?: SessionsClient.RequestOptions,
1313-
): Promise<core.WithRawResponse<unknown>> {
1304+
): Promise<core.WithRawResponse<core.BinaryResponse>> {
13141305
const { mount_id: mountId, path } = request;
13151306
const _queryParams: Record<string, unknown> = {
13161307
path,
@@ -1321,7 +1312,7 @@ export class SessionsClient {
13211312
this._options?.headers,
13221313
requestOptions?.headers,
13231314
);
1324-
const _response = await core.fetcher({
1315+
const _response = await core.fetcher<core.BinaryResponse>({
13251316
url: core.url.join(
13261317
(await core.Supplier.get(this._options.baseUrl)) ??
13271318
(await core.Supplier.get(this._options.environment)) ??
@@ -1331,6 +1322,7 @@ export class SessionsClient {
13311322
method: "GET",
13321323
headers: _headers,
13331324
queryParameters: { ..._queryParams, ...requestOptions?.queryParams },
1325+
responseType: "binary-response",
13341326
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
13351327
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
13361328
withCredentials: true,

0 commit comments

Comments
 (0)