How to handle AJAX response when broken pipe happens?
Asked Answered
F

3

8

I have a (Spring Boot 1.5) REST server handling file uploads (multipart/form-data) and a JS client using jQuery fileupload. In most cases it works fine but when I want to upload a larger file (about 4MB), the server figures out it is above the limit and sends back a response including the error message.

However it seems the server stops reading the request (which is of course right) which results in a broken pipe on client side. In this case the response is not handled. The fail callback is called with the following content of data.jqXHR (data.response is undefined):

{"readyState":0,"responseText":"","status":0,"statusText":"error"}

When doing the call using curl, the result is:

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8

curl: (55) Send failure: Broken pipe
{"files":[{"size":3863407,"error":"upload_uploadSizeExceeded"}]}

So there is a response returned but it seems to be ignored by the JS client. Is there any option to make jQuery handle the response even if the request is sent only partially?

BTW: even more strange, I see the request repeated several times in the server log in such a case, maybe a kind of retry mechanism in JS?

Falla answered 22/8, 2018 at 11:4 Comment(2)
Have you tried with Axios? github.com/axios/axiosLatten
@EduardoAguad no, but would require some efforts to exchange and doc looks not very promising about such kind of error handling, but will have a look, thanksFalla
F
3

Is there any option to make jQuery handle the response even if the request is sent only partially?

Short Answer

No, browsers use XMLHttpRequest and Fetch API, and this is considered a network error, by the specification, and network errors are intentionally empty.

CURL does not handle responses according to XMLHttpRequest specification.

Long Answer

Simulated Server

Read the request's ReadableStream and cancel mid-way:

const http = require('http');

const server = http.createServer((req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Content-Type', 'text/plain');

    //Close Request ReadableStream Prematurely
    //to simulate close pipe on exceed file size
    if (req.url === '/data' && req.method === 'POST') {
        console.log('simulating...');

        let i = 0;
        req.on('data', chunk => {
            if (i++ === 10)
                req.destroy('broken pipe');
        });
    }

    res.end('fooby\n');
}).listen(8080);

Client Tests

Method 1: XMLHttpRequests

I carefully inspected each event and there is no indication of Sent bytes. If we did have a Sent bytes value, which should be in loaded, we could know if the request was cancelled midway to handle this case without the response:

let req = new XMLHttpRequest();
req.open('POST', 'http://localhost:8080/data');
    req.onloadstart = function (event) {
            console.log(event);
    };
    req.onprogress = function (event) {
            console.log(event);
    };
    req.onabort = function (event) {
            console.log(event);
    };
    req.onerror = function (event) {
            console.log(event);
    };
    req.onload = function (event) {
            console.log(event);
    };
    req.ontimeout = function (event) {
            console.log(event);
    };
    req.onloadend = function (event) {
            console.log(event);
    };
    req.onreadystatechange = function (event) {
            console.log(event);
    };
req.send(new ArrayBuffer(100000000));

Unfortunately, nothing.

The read-only XMLHttpRequest.status property returns the numerical status code of the response of the XMLHttpRequest. status will be an unsigned short. Before the request is complete, the value of status will be 0. It is worth noting that browsers report a status of 0 in case of XMLHttpRequest errors too.

From the XMLHttpRequest specification:

A response whose type is "error" is known as a network error.

A network error is a response whose status is always 0, status message is always the empty byte sequence, header list is always empty, body is always null, and trailer is always empty.

Method 2: Fetch API

I was hoping to intercept a low-level ReadableStream and get something, but, unfortunately, the resolve callback is not called on network errors:

fetch('http://localhost:8080/data', {
        method: 'POST',
        body: new ArrayBuffer(100000000),
        mode: 'cors'
}).then(resp => {
        console.log(resp);
        //hopefully we can get readable stream here
        //...nope, networkerrors do not trigger resolve
}).catch(error => {
        console.log(error);//TypeError: "NetworkError when attempting to fetch resource."
}).then(retry => {
        console.log(retry);
});

A fetch() promise rejects with a TypeError when a network error is encountered, although this usually means a permissions issue or similar. An accurate check for a successful fetch() would include checking that the promise resolved, then checking that the Response.ok property has a value of true. An HTTP status of 404 does not constitute a network error.

Fetch Documentation

Fetch Specification

Browsers do not treat this as an HTTP error, but a network error, and therefore do not forward anything HTTP related to user code.

Conclusion

XHR and Fetch specification states that network errors are handled as empty responses.

Fructification answered 21/9, 2018 at 5:9 Comment(2)
This is not what I hoped for but thanks for your efforts and the well founded abstract of the state! Is there any option to distinguish between network errors (like connection refused, broken pipe, etc)?Falla
Unfortunately, no, Arne. The response is empty and the events provide no distinguishable information. The server will have to handle this for browsers.Fructification
R
0

Have you tried adding/tweaking this in your application.properties?

spring.servlet.multipart.max-file-size=1MB # Max file size. Values can use the suffixes "MB" or "KB" to indicate megabytes or kilobytes, respectively.
spring.servlet.multipart.max-request-size=10MB # Max request size. Values can use the suffixes "MB" or "KB" to indicate megabytes or kilobytes, respectively.

I don't know what you mean by how to handle the failure since the fail callback was executed in the client side (Technically that's already where you will handle the AJAX failure response). I think it's fair to say that you should just show an error modal/popup to your user in the fail code block.

Related answered 15/9, 2018 at 0:36 Comment(3)
The problem is not on server side but on handling the case on client side!Falla
Have you a control on the upload file field ? A good practice is to check size of your file by your application (on submit) and send the corresponding error message if it exceeds the limit. In that way, you will get the response from your application without having a server error that can be difficult to deal with...Alitaalitha
Yes, but there is a server side only check for remaining disk space and my app is handling GB uploads so client side only does not workFalla
J
0

Have you tried jQuery $.post to handle the fail response?

$.post('server.php', {deviceId: id})
.done( function(msg) { ... } )
.fail( function(xhr, textStatus, errorThrown) {
    console.log(xhr.responseText);
});
Jayson answered 21/9, 2018 at 2:39 Comment(1)
That's the problem, there is no call to fail()Falla

© 2022 - 2024 — McMap. All rights reserved.