pSquared
pSquared

Reputation: 87

JavaScript fetch automatically retries file streaming requests

I am running into a strange issue and I don't quite understand if this is expected or if this is a bug caused by me and if so how to fix it.

The problem:

When using the fetch function in javascript to upload a file to my server, if the server rejects the request and returns with 4xx or 5xx status codes, the fetch function or something deeper in javascript seems to make the request again instantly multiple times.

I have tracing enabled on my server and I'm able to see the request is retried almost instantly after it is rejected:

2025-02-25T13:41:15.276671Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-02-25T13:41:15.276853Z ERROR request{method=POST uri=/upload_file version=HTTP/1.1}: web_servicer_library::axum_server::web_api_request_processor: error_message="Request failed validation: filename size exceeds 50"
2025-02-25T13:41:15.276996Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=400
2025-02-25T13:41:15.277970Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-02-25T13:41:15.278064Z ERROR request{method=POST uri=/upload_file version=HTTP/1.1}: web_servicer_library::axum_server::web_api_request_processor: error_message="Request failed validation: filename size exceeds 50"
2025-02-25T13:41:15.278171Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=400
2025-02-25T13:41:15.279220Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-02-25T13:41:15.279310Z ERROR request{method=POST uri=/upload_file version=HTTP/1.1}: web_servicer_library::axum_server::web_api_request_processor: error_message="Request failed validation: filename size exceeds 50"
2025-02-25T13:41:15.279411Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=400
2025-02-25T13:41:15.280077Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-02-25T13:41:15.280153Z ERROR request{method=POST uri=/upload_file version=HTTP/1.1}: web_servicer_library::axum_server::web_api_request_processor: error_message="Request failed validation: filename size exceeds 50"
2025-02-25T13:41:15.280237Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=400
2025-02-25T13:41:15.282600Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-02-25T13:41:15.282678Z ERROR request{method=POST uri=/upload_file version=HTTP/1.1}: web_servicer_library::axum_server::web_api_request_processor: error_message="Request failed validation: filename size exceeds 50"
2025-02-25T13:41:15.282767Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=400
2025-02-25T13:41:23.860972Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-02-25T13:41:23.877111Z DEBUG request{method=GET uri=/_app/immutable/assets/ProximaNova-Semibold.BI3UiExG.otf version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-02-25T13:41:23.878281Z DEBUG request{method=GET uri=/_app/immutable/assets/ProximaNova-Semibold.BI3UiExG.otf version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=1 ms status=200
2025-02-25T13:41:24.525978Z DEBUG request{method=POST uri=/upload_file version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=665 ms status=200

This seems to happen an arbitrary number of times. Sometimes it happens only once. This behaviour does not happen for other API requests nor does it seems to ever happen when the response is a success leading me to speculate that it's related to the file streaming request.

Maybe because my server does not start reading the request and rejects it instead, JavaScript under the hood tries to make the request again? This happens on other browsers too btw, not just Chrome. Does anyone have any idea why this would be happening?

Background and setup:

Backend:

The main function of the server is basically something like this (minus all the setup of axum, server routing and Rust trait implementations):

pub async fn upload_file(
    headers: HeaderMap,
    req: Request<axum::body::Body>,
) -> impl IntoResponse {
    // Code to check headers and reject if invalid.
    
    // Code to create directory and file and return if error
    
    // If a size of the body is provided, check early.
    let mut body: axum::body::Body = req.into_body();
    if body
        .size_hint()
        .upper()
        .is_some_and(|size| size > FILE_UPLOAD_FILE_MAX_SIZE_IN_BYTES.try_into().unwrap())
    {
        // return error.
    }
    
    while let Some(chunk) = body.frame().await {
        match chunk {
            Ok(frame) => {
                if let Some(data) = frame.data_ref() {
                    file_size_in_bytes_tally += data.len();

                    // File size check.
                    if file_size_in_bytes_tally > FILE_UPLOAD_FILE_MAX_SIZE_IN_BYTES {
                        // return error.
                    }
                        
                    // Write and check its result.
                    if let Err(e) = file.write_all(data).await {
                        // return error.
                    }       
                }
            }
            Err(e) => {
                // return error.
            }
        }
    }
    
    // return success
}

Frontend:

/**
 * Helper function to make a request to the server with a file as the body.
 * @param urlAndMethod The URL and HTTP method to call for this request
 * @param file The file to stream over to the server
 * @returns A response from the server
 */
export async function MakeServerRequestWithFileBody(
    urlAndMethod: UrlAndMethodPair,
    file: File
): Promise<Response> {
    const headers = new Headers();
    headers.append(FILE_UPLOAD_NAME_HTTP_HEADER_KEY, file.name);

    let request: RequestInit = {
        method: urlAndMethod.method,
        headers: headers,
        body: file,
    };

    const response = await fetch(urlAndMethod.url, request);

    if (response.ok == false) {
        console.error(
            "Server could not handle " +
                urlAndMethod.url +
                " request, result code: " +
                response.status +
                " with accompanying text: " +
                response.statusText,
            request,
            response
        );
    } else {
        console.log(
            "Made successful request with to " + urlAndMethod.url,
            request,
            response
        );
    }

    return response;
}

Upvotes: 0

Views: 61

Answers (0)

Related Questions