hey-api / openapi-ts

🚀 The OpenAPI to TypeScript codegen. Generate clients, SDKs, validators, and more. Support: @mrlubos
https://heyapi.dev
Other
1.24k stars 96 forks source link

Can't manage to download attachment file #804

Open Sulray opened 3 months ago

Sulray commented 3 months ago

Description

I've seen another issue recently with a similar subject but the context is different so we'll see.

I want to download an excel report file from my frontend but I don't know why it's not working as I call the method generated in my client.

Here is a screenshot showing that it's working when i'm directly calling the endpoint from my autogenerated swagger: image

And here are the response headers I receive when I click on my button (with method "onClick") in my frontend: image

Even if the headers are the same (with content-disposition: attachment...), it's not downloading the file from my browser when using the frontend.

Reproducible example or configuration

Method to generate the excel file in the fastapi backend:

@router.get("/excel", response_class=StreamingResponse)
async def download_excel_report(background_tasks: BackgroundTasks, db=Depends(get_db)):
    """
    Get xlsx (excel) file of the current state of objects in database
    """
    df = await generate_panda_report(db=db)  # return a panda dataframe
    buffer = BytesIO()
    with pd.ExcelWriter(buffer) as writer:
        df.to_excel(excel_writer=writer, index=False)
    background_tasks.add_task(cleanup, buffer)
    return StreamingResponse(
        BytesIO(buffer.getvalue()),
        media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        headers={
            "Content-Disposition": f"attachment; filename=report.xlsx"
        },
    )

def cleanup(buffer: BytesIO) -> None:
    buffer.close()

Npm command to generate the client from autogenerated openapi doc:

"generate-client": "NODE_TLS_REJECT_UNAUTHORIZED=0 openapi-ts --input https://backend_ip/openapi.json --output ./src/client --client axios"

In the generated client:

export class ReportService {    
    /**
     * Download Excel Report
     * Get xlsx (excel) file of the current state of objects in database
     * @returns unknown Successful Response
     * @throws ApiError
     */
    public static downloadExcelReportReportExcelGet(): CancelablePromise<$OpenApiTs['/report/excel']['get']['res'][200]> {
        return __request(OpenAPI, {
            method: 'GET',
            url: '/report/excel'
        });
    }

}

In React, method defined as "onClick" on a button:

    const handleDownloadExcelReport = async(): Promise<void> => {
        try {
            const updatePromise = ReportService.downloadExcelReportReportExcelGet()
            await updatePromise
        } catch(error:unknown) {
            console.error(`Failed to download file: ${error}`);
        }
    }

OpenAPI specification (optional)

No response

System information (optional)

mrlubos commented 3 months ago

Hey @Sulray, can you add a portion of your OpenAPI specification?

Sulray commented 3 months ago

Yes for sure @mrlubos, here is an extract concerning the endpoint i'm talking about:

{
   "openapi":"3.1.0",
   "info":{
      "title":"Web project",
      "version":"0.1.0"
   },
   "paths":{
      "/report/excel":{
         "get":{
            "tags":[
               "report"
            ],
            "summary":"Download Excel Report",
            "description":"Get xlsx (excel) file of the current state of objects in database",
            "operationId":"download_excel_report_report_excel_get",
            "responses":{
               "200":{
                  "description":"Successful Response"
               }
            },
            "security":[
               {
                  "OAuth2AuthorizationCodeBearer":[

                  ]
               }
            ]
         }
      }
   }
}

It is autogenerated by my fastapi backend

mrlubos commented 3 months ago

@Sulray While I don't discount this might be a bug in Hey API (based on the other issue you linked), you'll also need to do some work on your end. You can see your response doesn't contain anything apart from status and description. You will want to expose the headers from your endpoint implementation in the OpenAPI specification (Content-Disposition, Content-Type, etc)

mrlubos commented 3 months ago

@Sulray if it's a public API you could send me the link too so I can have a look, not sure how Swagger UI knows it's a downloadable file

Sulray commented 3 months ago

@mrlubos Yes I was also thinking that my openapi specification should have more details you're right but since it was working from my swagger (and also if I access directly the endpoint from a new tab if i'm authenticated on backend) I was a bit lost.

Would you have any idea of where I could look to know how to give more information to my fastapi backend in order to have a more complete openapi specification ? What would be necessary from hey api point of view ? The fastapi documentation is not really exhaustive about StreamingResponse (which i'm using since I don't want to store locally the generated report so i'm using a buffer, not possible with FileResponse)

And unfortunately the api is not public

Sulray commented 3 months ago

Hi @mrlubos, I've managed to edit my openapi specification

I've put "type": "string" and "format": "binary" according to OpenApi v3 specification Now it looks like this:

{
   "openapi":"3.1.0",
   "info":{
      "title":"Web project",
      "version":"0.1.0"
   },
   "paths":{
      "/report/excel":{
         "get":{
            "tags":[
               "report"
            ],
            "summary":"Download Excel Report",
            "description":"Get xlsx (excel) file of the current state of objects in database",
            "operationId":"download_excel_report_report_excel_get",
            "responses":{
               "200":{
                  "description":"Successful Response",
                  "content":{
                     "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{
                        "schema":{
                           "type":"string",
                           "format":"binary",
                           "title":"File"
                        }
                     }
                  }
               }
            },
            "security":[
               {
                  "OAuth2AuthorizationCodeBearer":[

                  ]
               }
            ]
         }
      }
   }
}

The headers of the response are the same but for example here in the swagger the expected response is more precise image

Then I use the generated method like this: const response = await ReportService.downloadExcelReportReportExcelGet() This method still do not download the file automatically in the browser of the user so I wanted to use a "createObjectURL" method.

The response is supposed to be of one of these types on my frontend: const response: Blob | File However after running some tests it is taken as a string, I don't get how the type can be none of the expected types

And generating a blob as followed gives to the possibility to use createObjectURL to download the file but it leads to corrupted files that cannot be opened : let blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });

update: i made it work by using axios so should be possible at the end

dxm1337 commented 2 months ago

@Sulray
This is quite an unpleasant bug. I encountered it today as well and spent several hours dealing with it. I just ended up with a corrupted file. I also manually reworked it using axios.

The reason is simple: openapi-ts doesn't generate the year for processing blobs. If you go in and manually add responseType: 'blob' to the requestConfig of the sendRequest method, then the files will be processed just fine, etc. However, this is a workaround and only for binary files. And it's not the right solution (especially considering that this part of the code is generated).

It seems there is a solution, and I hope the developers will add it to the tasks for future releases.

mrlubos commented 2 months ago

@dxm1337 I'm not sure how easy it would be to fix for the legacy clients, but definitely would want to make sure this works for the standalone clients if that's not the case already. I believe this will be fixed in the next Fetch API client release.

I'd need to look at Axios and probably do some thinking as it doesn't have the concept of automatically detecting an appropriate response type

dxm1337 commented 2 months ago

@mrlubos I haven't delved deeply into the openapi-ts code (my qualifications are still somewhat lacking 😄 ), but perhaps something like this could be implemented in ApiRequestOptions:

responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream';

Then, as I understand it, we would be able to specify during method generation something like: responseType: 'blob', For older clients, it would remain by default as text/json or whatever it is currently.

But there remains a problem on the FastAPI side. They don't generate an indication of the return type for FileResponse in openapi.json. However, the /docs somehow handle it well. I started digging into the SwaggerUI source code, but I ran out of energy for now.