Azure / azure-functions-python-library

Azure Functions Python SDK
MIT License
151 stars 63 forks source link

Python - azure.functions.WsgiMiddleware does not support multiple Set-Cookie response headers #107

Closed bittebak closed 2 years ago

bittebak commented 2 years ago

Bug

When a HttpResponse has multiple headers with the same name, only one header is returned. Django uses multiple response cookies (e.g. 'csrf' and 'messages'). Multiple Set-Cookie headers can be returned. Set-Cookie is the name (the key) of the Http Response Header.

Context

A Django app running in an Azure Function

package

Name: azure-functions
Version: 1.8.0
Summary: Azure Functions for Python
Home-page: UNKNOWN
Author: Microsoft Corporation
Author-email: azpysdkhelp@microsoft.com

Version

Azure function runtime version = ~4

Reproduction

  1. Create a http response with two Set-Cookie headers This header has been captured from the app running without the Azure wsgi middleware.
HTTP/1.1 200 OK
Date: Sun, 16 Jan 2022 09:00:59 GMT
Server: WSGIServer/0.2 CPython/3.9.9
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
Vary: Cookie
Content-Length: 5206
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Set-Cookie:  messages=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Max-Age=0; Path=/; SameSite=Lax
Set-Cookie:  csrftoken=L6coOZOclDhAg2X7geBKbhvJSHPxz9AbgOkoVqNAIsgVe8oxTEPTSPbc2VPSyjwq; expires=Sun, 15 Jan 2023 09:00:59 GMT; Max-Age=31449600; Path=/; SameSite=Lax
  1. Run without azure.functions.WsgiMiddleware Two Set-Cookies headers are returned (as depicted above)

  2. Run with azure.functions.WsgiMiddleware.handle

    import azure.functions as func
    def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    http_response = func.WsgiMiddleware(application).handle(req, context)
    return http_response

Only one header with the name "Set-Cookie" is returned.

Local and remote

This behaviour has been reproduced both locally (while debugging) as on an Azure Function container (icarus-int)

Root cause

Python package 'azure-functions' azure\functions_http.py

class HttpResponseHeaders(BaseHeaders, collections.abc.MutableMapping):

    def __setitem__(self, key: str, value: str):
        self.__http_headers__[key.lower()] = value

    def __delitem__(self, key: str):
        del self.__http_headers__[key.lower()]

The headers are stored as a dictionary. All keys in a dictionary are unique.

References

https://www.rfc-editor.org/rfc/rfc6265#page-6 Section 3

"Origin servers SHOULD NOT fold multiple Set-Cookie header fields into a single header field. The usual mechanism for folding HTTP headers fields (i.e., as defined in [RFC2616]) might change the semantics of the Set-Cookie header field because the %x2C (",") character is used by Set-Cookie in a way that conflicts with such folding."

https://www.rfc-editor.org/rfc/rfc6265#section-4.1 "Servers SHOULD NOT include more than one Set-Cookie header field in the same response with the same cookie-name. (See Section 5.2 for how user agents handle this case.)"

This means that multiple Set-Cookie headers with different cookie-names are allowed.

v-bbalaiagar commented 2 years ago

Hi @pragnagopa / @fabiocav, Can you please suggest how to proceed here

pragnagopa commented 2 years ago

@v-bbalaiagar - please move this issue to https://github.com/Azure/azure-functions-python-worker/issues cc @vrdmr

v-bbalaiagar commented 2 years ago

Transferring this issue to python worker repo for further investigation.

shreyabatra4 commented 2 years ago

related issues:

Ben-Ruben commented 2 years ago

Having the same issue. Is there any timeline available on if/when this will be available? Or would it be better to rewrite my functions to e.g. Node where setting multiple cookies seems to be available?

Th3OnlyN00b commented 11 months ago

Leaving this here for anyone like myself who is searching for how to do this, finds this as the only relevant google search result, and can't figure out how to do it. Here's example code that is working.

response = func.HttpResponse(json.dumps({'code': 'success', 'message': "User logged in"}), status_code=200)
# Headers have to be added by this method as the default is a dict, which doesn't work because dict keys are unique
response.headers.add("Set-Cookie", f"token={token}")
response.headers.add("Set-Cookie", f"id={uuid}")
aadamsx commented 11 months ago

Leaving this here for anyone like myself who is searching for how to do this, finds this as the only relevant google search result, and can't figure out how to do it. Here's example code that is working.

response = func.HttpResponse(json.dumps({'code': 'success', 'message': "User logged in"}), status_code=200)
# Headers have to be added by this method as the default is a dict, which doesn't work because dict keys are unique
response.headers.add("Set-Cookie", f"token={token}")
response.headers.add("Set-Cookie", f"id={uuid}")

Thanks for the code, but this ended up not working. You'll see the Set-Cookies in the header response, but if you'll notice in the cookie's tab, only one cooke is set, and since it's a dictionary, the second Set-Cookie overwrites the values of the first Set-Cookie.

Th3OnlyN00b commented 11 months ago
image image

Odd, that works perfectly fine for me. Behind the scenes it's not a dictionary actually, it's a list of tuples as seen in line 1138 of the Headers object file: self._list.append((_key, _value))

(Both of the credentials in the screenshot are now invalid btw, don't worry)

aadamsx commented 11 months ago

Here's what I'm seeing:

image

The client is a stock create react app running locally on https://127.0.0.1:3000 and using Chrome dev tools.

I'm using stock azure functions v4 with no middle where or extra imports -- maybe I need to add something?

So in terms of cookies, both show in the response, but for cookies set, only the first ever shows as valid by chrome. In prior iterations, I would see that the values for the first token would be overwritten by the second.

Th3OnlyN00b commented 11 months ago

This is an issue with chrome, not your code. https://stackoverflow.com/questions/63204093/how-to-get-set-multiple-set-cookie-headers-using-fetch-api

Officially, there is no requirement to support multiple Set-Cookie headers in a response, so chrome's auto-parsing of your headers is deleting the second one (but the standard arguably says it could keep either one). If you try this with Postman or any other request client that supports multiple headers of the same name, you'll find you're getting both cookies set. Remember that how the headers are handled is entirely on the client, and you have full control over that in your client code.