Search code examples
javascriptpostsafarifetch-api

Browser (Safari) refuses to send the body via Javascript fetch API with POST


[UPDATE: this only happens with Safari, it turns out, not with Chrome or Firefox]

This question has been posted a million times all over the web, but all the tips, hints, and tricks (cors, content-type, etc.) haven't brought me to a solution.

To isolate, I am testing this with a very small python 'web server' that has this:

def handle_request(request):
    print( "REQUEST TO HANDLE")
    print( request)
    if request.startswith("GET /checkdo.html HTTP/1.1"):
        if checkdo is not None:
            response = "HTTP/1.1 200 OK\r\n"
            response += "Content-Type: text/html\r\n"
            response += "Content-Length: {}\r\n".format(len(checkdo))
            response += "\r\n" + checkdo
        else:
            response = "HTTP/1.1 404 Not Found\r\n\r\n"
            response += "No data found for the token."
    elif request.startswith("POST /checkdo-post HTTP/1.1"):
        token = extract_token(request)
        if is_authorized(token):
            data = request.split("\r\n")[-1]
            print( "DATA")
            print( data)

            store_data(token, data)

            response = "HTTP/1.1 200 OK\r\n\r\n"
            response += "Data saved successfully!"
        else:
            response = "HTTP/1.1 403 Forbidden\r\n\r\n"
            response += "Unauthorized access!"
    else:
        response = "HTTP/1.1 404 Not Found\r\n\r\n"
    return response

def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('', 8080)
    server_socket.bind(server_address)
    server_socket.listen(1)
    print("Server listening on port 8080...")

    while True:
        client_socket, client_address = server_socket.accept()
        print("Received connection from:", client_address)

        rawrequest = client_socket.recv(4096)
        print( "RECEIVE")
        print( rawrequest)
        request = rawrequest.decode('utf-8')

        response = handle_request(request)

        print( "RESPOND")
        print( response)
        client_socket.sendall(response.encode('utf-8'))

        client_socket.close()

if __name__ == "__main__":
    start_server()

Basically, this removes all the intelligence out of the backend, it is now simply a tcp socket.

The GET works. But when I send a POST in the web page javascript:

        const saveToServer = (remindersToSave) => {
            const myHeaders = new Headers();
            const myToken = getToken();
            myHeaders.append( "Accept", "application/json");
            myHeaders.append( "Content-type", "application/json; charset=UTF-8");
            //myHeaders.append( "Content-type", "application/x-www-form-urlencoded");
            myHeaders.append( "Authorization", myToken);
            const myURL = window.location.protocol + "//" + window.location.host + "/checkdo-post";
            const myData = JSON.stringify(remindersToSave);
            console.log( "POST: " + myURL);
            console.log( "DATA: " + myData);
            (async () => {
                const rawResponse = await fetch(myURL, {
                    method: 'POST',
                    headers: myHeaders,
                    body: myData
                });
                console.log(rawResponse);
            })();
        }

I notice in the browser console:

[Log] POST: http://localhost:8080/checkdo-post (checkdo.html, line 81)
[Log] DATA: [{"name":"A Task","subtasks":[{"name":"A Subtask","lastDone":0},{"name":"A Subtask","lastDone":1687125600000},{"name":"A Subtask","lastDone":1687125600000}],"lastDone":0}] (checkdo.html, line 82)
[Log] Response {type: "basic", url: "http://localhost:8080/checkdo-post", redirected: false, status: 200, ok: true, …} (checkdo.html, line 96)

Basically, my javascript code says it is going to send that POST request with fetch.

But on the python nano-'server' end I see

Received connection from: ('127.0.0.1', 55691)
RECEIVE
b'POST /checkdo-post HTTP/1.1\r\nHost: localhost:8080\r\nAccept: application/json\r\nAuthorization: B4Kr6H9AqPsuKfpc3RuGimafae8Xc6mT9hbsRmzz\r\nSec-Fetch-Site: same-origin\r\nAccept-Language: en-GB,en;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nSec-Fetch-Mode: cors\r\nContent-Type: application/json; charset=UTF-8\r\nOrigin: http://localhost:8080\r\nContent-Length: 171\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15\r\nReferer: http://localhost:8080/checkdo.html\r\nConnection: keep-alive\r\nSec-Fetch-Dest: empty\r\nCookie: default-theme=ngax\r\n\r\n'
REQUEST TO HANDLE
POST /checkdo-post HTTP/1.1
Host: localhost:8080
Accept: application/json
Authorization: B4Kr6H9AqPsuKfpc3RuGimafae8Xc6mT9hbsRmzz
Sec-Fetch-Site: same-origin
Accept-Language: en-GB,en;q=0.9
Accept-Encoding: gzip, deflate
Sec-Fetch-Mode: cors
Content-Type: application/json; charset=UTF-8
Origin: http://localhost:8080
Content-Length: 171
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15
Referer: http://localhost:8080/checkdo.html
Connection: keep-alive
Sec-Fetch-Dest: empty
Cookie: default-theme=ngax


DATA

RESPOND
HTTP/1.1 200 OK

Data saved successfully!

There is no data in the request received. Given that my 'web server' really isn't a web server, it is just a raw tcp socket, it must be that my Safari browser refuses to send the data in the POST. But whatever I do, I am unable to solve this.

I am certain it is the browser as:

curl -i -X POST -H 'Content-Type: application/json' -d '{"name": "New item", "year": "2009"}' http://localhost:8080/checkdo-post

results in:

b'POST /checkdo-post HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.0.1\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 36\r\n\r\n{"name": "New item", "year": "2009"}'
REQUEST TO HANDLE
POST /checkdo-post HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.0.1
Accept: */*
Content-Type: application/json
Content-Length: 36

{"name": "New item", "year": "2009"}
DATA
{"name": "New item", "year": "2009"}
RESPOND
HTTP/1.1 200 OK

Data saved successfully!

In my nano-web-server


Solution

  • Ok so I had the same exact problem, I looked into it a lot, here is what i tried:

    1. Changing my CORS headers to allow the specifics in the requests
    2. Caching my ssl certificate
    3. Making the api url more specific in the request by adding a trailing '/' at the end.

    Those might help but at the end my problem was simply fixed by asking my tcp socket to continue receiving the request data until the body is equal to the content length:

    headers, body = request_data.split("\r\n\r\n", 1)  
    
    content_length = 0
    for line in headers.split("\r\n"):
        if "Content-Length" in line:
            content_length = int(line.split(": ")[1])
            break
    
    while len(body) < content_length:
        body += conn.recv(1024).decode('UTF-8')
    
    • make sure to decode the request data to utf-8 if you haven't already unless you are sending file binary data directly do not decode until you split the body first

    Your web server did NOT receive the full request YET, safari sends it on chunks for some reason, hope this helps, keep receiving the request until the body of the POST is received.