hey-api / openapi-ts

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

Unable to prompt browser to save file with 'content-disposition: attachment;' #796

Open warrenbuckley opened 3 months ago

warrenbuckley commented 3 months ago

Description

Problem

Using the generated client the browser is not prompting the user to auto download the file.

Response Headers

api-supported-versions: 1.0 
 content-disposition: attachment; filename=VideoReport.csv; filename*=UTF-8''VideoReport.csv 
 content-length: 56 
 content-type: text/csv 
 date: Thu,18 Jul 2024 18:40:10 GMT 
 server: Kestrel 

Swagger UI

image

Reproducible example or configuration

Default config just using simple args as a node script

"generate-api": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 openapi-ts --input https://localhost:44344/umbraco/swagger/NewsRoom/swagger.json --output src/api"
import { LitElement, css, html, customElement } from "@umbraco-cms/backoffice/external/lit";
import { UMB_NOTIFICATION_CONTEXT, UmbNotificationContext } from "@umbraco-cms/backoffice/notification";
import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UUIButtonElement } from "@umbraco-cms/backoffice/external/uui";
import { generateCollectionsReport, generateEventsReport, generateVideoReport } from "../../api";

@customElement('reports-dashboard')
export class ReportsDashboardElement extends UmbElementMixin(LitElement) {

    #notificationContext?: UmbNotificationContext;

    constructor() {
        super();
        this.consumeContext(UMB_NOTIFICATION_CONTEXT, (_instance) => {
            this.#notificationContext = _instance;
        });
    }

    private async _download(reportType: string,  ev: Event){

        const buttonElement = ev.target as UUIButtonElement;
        buttonElement.state = "waiting";

        switch(reportType){
            case 'videos':
                await generateVideoReport().then(response => {
                    response
                })
                break;

            case 'events':
                await generateEventsReport();
                break;

            case 'collections':
                await generateCollectionsReport();
                break;
        }

        // Set the state of the clicked button back to sucess
        buttonElement.state = "success";

        // Fire a friendly notification
        this.#notificationContext?.peek("positive", {
            data: {
                headline: "Download Ready",
                message: `Your ${reportType} report is ready for download`
            }
        });
    }

    render() {
        return html`
        <uui-box headline="Newsroom Reporting">
            <p>Here you are able to download CSV reports as required</p>

            <uui-button look="primary" label="Download Videos" @click=${(ev: Event) => this._download('videos', ev)}>
                Download list of all Videos
            </uui-button>

            <uui-button look="primary" label="Download Events"  @click=${(ev: Event) => this._download('events', ev)}>
                Download list of all Events
            </uui-button>

            <uui-button look="primary" label="Download Collections"  @click=${(ev: Event) => this._download('collections', ev)}>
                Download list of all Collections
            </uui-button>

        </uui-box>
    `;
    }

    static styles = [
        UmbTextStyles,
        css`
            :host {
                display: block;
                //padding: var(--uui-box-default-padding);
                padding: 24px;
            }
        `,
    ];
}

export default ReportsDashboardElement;

declare global {
    interface HTMLElementTagNameMap {
        'reports-dashboard': ReportsDashboardElement;
    }
}
// This file is auto-generated by @hey-api/openapi-ts

import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
import type { GenerateCollectionsReportResponse, GenerateEventsReportResponse, GenerateVideoReportResponse } from './types.gen';

/**
 * Generates a simple CSV file and streams it back to the browser.
 * @returns unknown Returns the CSV file stream
 * @throws ApiError
 */
export const generateCollectionsReport = (): CancelablePromise<GenerateCollectionsReportResponse> => { return __request(OpenAPI, {
    method: 'GET',
    url: '/umbraco/newsroom/api/v1/GenerateCollectionsReport',
    errors: {
        401: 'The resource is protected and requires an authentication token'
    }
}); };

/**
 * Generates a simple CSV file and streams it back to the browser.
 * @returns unknown Returns the CSV file stream
 * @throws ApiError
 */
export const generateEventsReport = (): CancelablePromise<GenerateEventsReportResponse> => { return __request(OpenAPI, {
    method: 'GET',
    url: '/umbraco/newsroom/api/v1/GenerateEventsReport',
    errors: {
        401: 'The resource is protected and requires an authentication token'
    }
}); };

/**
 * Generates a simple CSV file and streams it back to the browser.
 * @returns unknown Returns the CSV file stream
 * @throws ApiError
 */
export const generateVideoReport = (): CancelablePromise<GenerateVideoReportResponse> => { return __request(OpenAPI, {
    method: 'GET',
    url: '/umbraco/newsroom/api/v1/GenerateVideoReport',
    errors: {
        401: 'The resource is protected and requires an authentication token'
    }
}); };

OpenAPI specification (optional)

{
    "openapi": "3.0.1",
    "info": {
        "title": "Newsroom Umbraco Backoffice API",
        "contact": {
            "name": "Warren Buckley",
            "url": "https://hackmakedo.com",
            "email": "warren@hackmakedo.com"
        },
        "version": "1.0"
    },
    "paths": {
        "/umbraco/newsroom/api/v1/GenerateCollectionsReport": {
            "get": {
                "tags": [
                    "Report Dashboard"
                ],
                "summary": "Generates a simple CSV file and streams it back to the browser.",
                "responses": {
                    "200": {
                        "description": "Returns the CSV file stream",
                        "content": {
                            "text/csv": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "type": "string",
                                            "format": "binary"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The resource is protected and requires an authentication token"
                    }
                },
                "security": [
                    {
                        "Backoffice User": []
                    }
                ]
            }
        },
        "/umbraco/newsroom/api/v1/GenerateEventsReport": {
            "get": {
                "tags": [
                    "Report Dashboard"
                ],
                "summary": "Generates a simple CSV file and streams it back to the browser.",
                "responses": {
                    "200": {
                        "description": "Returns the CSV file stream",
                        "content": {
                            "text/csv": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "type": "string",
                                            "format": "binary"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The resource is protected and requires an authentication token"
                    }
                },
                "security": [
                    {
                        "Backoffice User": []
                    }
                ]
            }
        },
        "/umbraco/newsroom/api/v1/GenerateVideoReport": {
            "get": {
                "tags": [
                    "Report Dashboard"
                ],
                "summary": "Generates a simple CSV file and streams it back to the browser.",
                "responses": {
                    "200": {
                        "description": "Returns the CSV file stream",
                        "content": {
                            "text/csv": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "type": "string",
                                            "format": "binary"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "The resource is protected and requires an authentication token"
                    }
                },
                "security": [
                    {
                        "Backoffice User": []
                    }
                ]
            }
        }
    },
    "components": {
        "securitySchemes": {
            "Backoffice User": {
                "type": "oauth2",
                "description": "Umbraco Authentication",
                "flows": {
                    "authorizationCode": {
                        "authorizationUrl": "/umbraco/management/api/v1/security/back-office/authorize",
                        "tokenUrl": "/umbraco/management/api/v1/security/back-office/token",
                        "scopes": {}
                    }
                }
            }
        }
    }
}

System information (optional)

mrlubos commented 3 months ago

@warrenbuckley is this a regression? Are you able to hack it yourself by supplying certain headers? Do you know what's the missing piece to enable the desired behaviour?

warrenbuckley commented 3 months ago

Yeh unsure if it's my API or clientside. Where I can see the correct header in the response I would have expected this to 'just' work.

mrlubos commented 3 months ago

Can you verify if the request sent with the client has the same headers/payload as the one sent through Swagger UI? That would be normally the first line of attack. If both request and response are identical, I'd check if the response isn't being transformed somehow before being returned. If you can hack it and find out what the culprit is, it will be much faster for me to fix

warrenbuckley commented 3 months ago

Yeh of course will investigate & let you know