#14770: Set cookies from request: Design decision needed

469 views
Skip to first unread message

Paul McLanahan

unread,
Nov 24, 2010, 1:31:35 PM11/24/10
to django-d...@googlegroups.com
Hi all,

I created ticket #14770 (http://code.djangoproject.com/ticket/14770)
because I ran into a situation this week where I only had access to
the request, but needed to set a cookie. As the ticket says, I did
solve this problem with a middleware, but I couldn't think of a reason
not to include this ability in core. So, I wrote up a patch complete
with docs and tests. Any feedback you might have would be very much
appreciated.

Thanks,

Paul McLanahan

Jacob Kaplan-Moss

unread,
Nov 24, 2010, 2:23:57 PM11/24/10
to django-d...@googlegroups.com
On Wed, Nov 24, 2010 at 12:31 PM, Paul McLanahan <pmcla...@gmail.com> wrote:
> I created ticket #14770 (http://code.djangoproject.com/ticket/14770)
> because I ran into a situation this week where I only had access to
> the request, but needed to set a cookie. As the ticket says, I did
> solve this problem with a middleware, but I couldn't think of a reason
> not to include this ability in core.

Ugh, I'm -1 on this. It confuses requests and responses, and abstracts
away a *very* important aspect of how HTTP actually works. Doing this
is semantically unclean and binds together two parts of the
request/response cycle that are separate by design. I don't at all
like the idea of obscuring the truth about how HTTP actually works.

There's a tiny chance I might be able to be convinced to change my
mind, but you'll have to back up and explain exactly what it is you're
trying to do that's impossible without this hack.

Jacob

Paul McLanahan

unread,
Nov 24, 2010, 2:50:20 PM11/24/10
to django-d...@googlegroups.com
On Wed, Nov 24, 2010 at 2:23 PM, Jacob Kaplan-Moss <ja...@jacobian.org> wrote:
> Ugh, I'm -1 on this. It confuses requests and responses, and abstracts
> away a *very* important aspect of how HTTP actually works. Doing this
> is semantically unclean and binds together two parts of the
> request/response cycle that are separate by design. I don't at all
> like the idea of obscuring the truth about how HTTP actually works.
>
> There's a tiny chance I might be able to be convinced to change my
> mind, but you'll have to back up and explain exactly what it is you're
> trying to do that's impossible without this hack.

There are just situations where you need to have the ability to cause
a cookie to be set, but can't yet create the response object. In my
case I was creating a small framework for registering functions that
would check for and set messages via the messages framework. If I
called the view and passed the request and response to these
functions, the messages they'd set wouldn't show until the next
response because the page would already have been rendered. If I could
stick to only passing in a request then I could put off getting the
response object until later, my functions could set cookies, and the
messages would show up at the right time.

In David Cramer's post on the subject[0] he mentions needing this when
building an authentication service. I'm sure there are many other use
cases and at least as many hacks that have been put into place to get
around this limitation.

I'm certainly not married to this implementation. It made sense to me
to offer methods with the exact same signature on the request as the
response, but I can see you point about confusion. Since they
accomplish the same outcome, I don't think any confusion would result
in bugs, but it could, so I understand.

Would you be more open to a completely separate cookie handling
component that the HttResponse.set_cookie and
HttResponse.delete_cookie called for backward compatibility? I have no
idea how such a thing would work or maintain state through a
request-response cycle, but if an alternate new API were created
through which one always interacted with cookies, perhaps that would
assuage some confusion concerns.

I mainly just need to be able to set a cookie from anywhere in a view,
or in any decorator or function that deals with the view process. The
only way I've found to do that is to use a middleware that
monkeypatches the request and then copies whatever was added there to
the resopnse on its way out. If there's another way, I'm very happy to
hear it. The thing I like most about attempting contributions like
this to Django is the learning experience.

Thanks for the feedback!

Paul

[0] http://www.davidcramer.net/code/62/set-cookies-without-a-response-in-django.html

Jacob Kaplan-Moss

unread,
Nov 24, 2010, 3:11:37 PM11/24/10
to django-d...@googlegroups.com
On Wed, Nov 24, 2010 at 1:50 PM, Paul McLanahan <pmcla...@gmail.com> wrote:
> There are just situations where you need to have the ability to cause
> a cookie to be set, but can't yet create the response object.

Okay, but you've said this twice without giving any examples. Which
makes it very easy for me to argue that in fact any trying to set a
cookie without a response object around is just a badly-designed or
-factored application. I believe that argument, so you're going to
need to show concrete examples.

> In my
> case I was creating a small framework for registering functions that
> would check for and set messages via the messages framework. If I
> called the view and passed the request and response to these
> functions, the messages they'd set wouldn't show until the next
> response because the page would already have been rendered. If I could
> stick to only passing in a request then I could put off getting the
> response object until later, my functions could set cookies, and the
> messages would show up at the right time.

I'm really not following this "next response because the page has
already been rendered" thing -- if you create the reponse object, pass
it into your utility functions, then return it that creates the
cookies just fine.

> In David Cramer's post on the subject[0] he mentions needing this when
> building an authentication service. I'm sure there are many other use
> cases and at least as many hacks that have been put into place to get
> around this limitation.

David's post uses this example::

def my_view(request):
request.COOKIES.set([args])
...
return response

How in any way is that better than::

def my_view(request):
response = HttpResponse(...)
response.COOKIES.set(...)
...
return response

Okay, it saves one line of code, but in the Python world you don't get
bonus points for shorter more obscure code.

> Would you be more open to a completely separate cookie handling
> component that the HttResponse.set_cookie and
> HttResponse.delete_cookie called for backward compatibility? I have no
> idea how such a thing would work or maintain state through a
> request-response cycle, but if an alternate new API were created
> through which one always interacted with cookies, perhaps that would
> assuage some confusion concerns.

Again, *why*? Cookies are just an HTTP response header. What purpose
does hiding HTTP's semantics serve?

> I mainly just need to be able to set a cookie from anywhere in a view,
> or in any decorator or function that deals with the view process. The
> only way I've found to do that is to use a middleware that
> monkeypatches the request and then copies whatever was added there to
> the resopnse on its way out. If there's another way, I'm very happy to
> hear it.

* Views::

def my_view(request):
response = HttpResponse(...)
...
response.write(...)
...
response.COOKIES.set(...)
...
return response

* Decorators::

def my_decorator(viewfunc):
def _inner(request, *args, **kwargs):
response = viewfunc(request, *args, **kwargs)
response.COOKIES.set(...)
return response
return _inner

* Arbitrary functions::

def some_function(response):
response.COOKIES.set(...)
...

It's possible I'm missing something, but I really can't see that some
sort of magic global cookie handling adds enough utility to outweigh
its unintuitiveness.

Of course, you're certainly free to implement some magic middleware
thing in your own code -- it's easy enough to hang something off the
request object then transfer it to the response on the way out. It
certainly wouldn't pass a code review if *I* was performing it, but
luckily your app doesn't have to live up to my standards :)

Jacob

Ian Kelly

unread,
Nov 24, 2010, 3:42:08 PM11/24/10
to django-d...@googlegroups.com
On Wed, Nov 24, 2010 at 1:11 PM, Jacob Kaplan-Moss <ja...@jacobian.org> wrote:
>> In my
>> case I was creating a small framework for registering functions that
>> would check for and set messages via the messages framework. If I
>> called the view and passed the request and response to these
>> functions, the messages they'd set wouldn't show until the next
>> response because the page would already have been rendered. If I could
>> stick to only passing in a request then I could put off getting the
>> response object until later, my functions could set cookies, and the
>> messages would show up at the right time.
>
> I'm really not following this "next response because the page has
> already been rendered" thing -- if you create the reponse object, pass
> it into your utility functions, then return it that creates the
> cookies just fine.

I gather that the OP is trying to do something like this:


def some_view(request):
f(request)
return some_response(request)

def f(request, response):
response.COOKIES.set(...)
messages.add_message(request, messages.INFO, "Cookie set")


The problem then is that function f needs the response object in order
to set the cookie, but if the response object is passed into f, then
it is too late to add the message. This makes it difficult for the
same function to both set a cookie and add a message, unless it is
also responsible for creating the response.

An alternate solution might be to use delayed execution to set the cookie:

def some_view(request):
closure = f(request)
response = some_response(request)
closure(response)
return response

def f(request):
messages.add_message(request, messages.INFO, "Cookie set")
def closure(response):
response.COOKIES.set(...)
return closure

Although this could cause confusion if the message gets added, and
then the view fails in some way before the cookie actually gets set.

Cheers,
Ian

Paul McLanahan

unread,
Nov 24, 2010, 4:01:48 PM11/24/10
to django-d...@googlegroups.com
Thanks for that example Ian. That's exactly it. I'm doing it from a
view decorator, but in effect it's the same. I need to call a function
from a view that needs to be able to set messages and potentially set
other cookies. I need the request object to set the messages, and the
response to set the cookies. This is already confusing in my opinion
as the messages framework potentially uses cookies, but that's beside
the point. If I get the response then pass it to my function, any
messages it sets will not be shown to the user on that response, but
would instead be saved and shown to the user on the subsequent
request. So I need a way to be able to cause cookies to be set from
such a function but before rendering the response. I thought about
having said function return some crazy list of dicts that I'd then
pass to response.set_cookie later, but that seemed even more hacky.

Jacob: I'm willing to accept that this situation is rare and that your
objections are valid. I can accomplish this via a middleware, and will
continue to do so. But I do still wish for a more simple and supported
way to deal with this situation. I can see Ian's delayed execution
pattern working just fine, but it's less readable in my opinion.

Thanks again,

Paul

Jacob Kaplan-Moss

unread,
Nov 24, 2010, 4:20:33 PM11/24/10
to django-d...@googlegroups.com
On Wed, Nov 24, 2010 at 3:01 PM, Paul McLanahan <pmcla...@gmail.com> wrote:
> I need to call a function
> from a view that needs to be able to set messages and potentially set
> other cookies. I need the request object to set the messages, and the
> response to set the cookies. This is already confusing in my opinion
> as the messages framework potentially uses cookies, but that's beside
> the point. If I get the response then pass it to my function, any
> messages it sets will not be shown to the user on that response, but
> would instead be saved and shown to the user on the subsequent
> request.

Okay, I think we're narrowing in on things. Just so I'm clear, you've
got a view that sets a message towards the beginning and then displays
that message in the template *on the very same request*, yes?

So in essence you're doing something like::

def my_view(request):
set_some_messages(request)
response = render_to_response(...)
set_some_cookies(response)
return resp

But you'd like to collapse `set_some_messages()` and
`set_some_cookies()` into a single function, so something like::

def my_view(request):
response = render_to_response(...)
set_some_cookies_and_messages(request, response)
return response

Does that about some it up?

Because if so, the problem has nothing to do with cookies. It's
happening because the messages are being fetched in the template,
which you're rendering into a response "too early". This in fact has
nothing to do with the request/response cycle or anything, but just
with the fact that you're fetching messages from a template into a
response. But remember: you can get a response without rendering a
template, so you could do something like this::

def my_view(request):
response = HttpResponse()
set_some_cookies_and_messages(request, response)
response.write(render_to_string(...))
return response

render_to_response is a shortcut that collapses a bunch of steps for
the common case, but does so at the expense of inflexibility.

Jacob

PS: Oh, and BTW, the reason you're seeing this problem with messages,
specifically, is that messages really aren't designed to be produced
and consumed in the same view. The canonical use case for messages is
for use in the redirect-after-POST pattern::

def my_view(request):
form = MyForm(request.POST or None)
if form.is_valid():
form.save()
messages.add_message("It worked!")
return redirect('success')
...

If you're producing and consuming messages in the same request you
might want a different tool, or you'll need to work around the
assumptions that the messages framework makes.

Paul McLanahan

unread,
Nov 24, 2010, 10:57:13 PM11/24/10
to django-d...@googlegroups.com
On Wed, Nov 24, 2010 at 4:20 PM, Jacob Kaplan-Moss <ja...@jacobian.org> wrote:
> Okay, I think we're narrowing in on things. Just so I'm clear, you've
> got a view that sets a message towards the beginning and then displays
> that message in the template *on the very same request*, yes?

Basically, yes.

> So in essence you're doing something like::
>
>    def my_view(request):
>        set_some_messages(request)
>        response = render_to_response(...)
>        set_some_cookies(response)
>        return resp
>
> But you'd like to collapse `set_some_messages()` and
> `set_some_cookies()` into a single function, so something like::
>
>    def my_view(request):
>        response = render_to_response(...)
>        set_some_cookies_and_messages(request, response)
>        return response
>
> Does that about some it up?

It's a simplification, but yes.

> Because if so, the problem has nothing to do with cookies. It's
> happening because the messages are being fetched in the template,
> which you're rendering into a response "too early". This in fact has
> nothing to do with the request/response cycle or anything, but just
> with the fact that you're fetching messages from a template into a
> response. But remember: you can get a response without rendering a
> template, so you could do something like this::
>
>    def my_view(request):
>        response = HttpResponse()
>        set_some_cookies_and_messages(request, response)
>        response.write(render_to_string(...))
>        return response
>
> render_to_response is a shortcut that collapses a bunch of steps for
> the common case, but does so at the expense of inflexibility.

Right. I do know that, but as I said before, this example is a
simplification. Just for completeness, I'll lay it all out for you.
I'm sure it won't change your mind, but hopefully it'll more clearly
demonstrate the dilema.

I have a set of views that I'd like to potentially show one or more
messages to the user. These messages will be created by one or more
functions that are registered into a module I wrote that finds these
special functions and provides a decorator. The decorator is applied
to the views where the messages should be displayed. These messages
are things like, "Your account will expire soon", and since they'll
keep displaying until the user renews, I provide a link for them to
hide the message. That hiding is accomplished by setting a cookie. I'm
also using a cookie to basically cache the fact that the user has
passed the test with no message and therefore doesn't need the check
for the remainder of their browser session. So, this decorator looks
something like:

def check_alerts(func):
def decor(request):
for alert in registered_alerts:
alert(request)
return func(request)
return decor

An example alert function might be:

def account_expiring(request):
if request.COOKIES.get('account_expiring') == 'no':
return
if request.user.get_profile().is_about_to_expire():
message.warning(request, "Your account will expire soon, yo")
else:
request.set_cookie('account_expiring', 'no')

That's obviously not exactly the code, but that's the idea. It works
great, and is the least convoluted thing I could come up with. I
thought of creating an empty response in the decorator, passing it to
the alert functions, then passing it to my decorated views and making
them use it instead of what they're already doing. But some are
generic views and some are not my code, so it'd be very difficult to
hack them all into accepting another argument, or using the decorator
to capture the response given by the view, then transfer cookies from
my fake response to the real one before returning it. All of that is
possible, but seemed more hacky to me than having the ability to set a
cookie from the request. I just thought that if I could get myself
into this situation then I'm sure many others have even more complex
systems, and since there'd already been a couple of solutions proposed
in the community, that perhaps it'd be useful for the framework to
have an easy solution.

> PS: Oh, and BTW, the reason you're seeing this problem with messages,
> specifically, is that messages really aren't designed to be produced
> and consumed in the same view. The canonical use case for messages is
> for use in the redirect-after-POST pattern::
>
>    def my_view(request):
>        form = MyForm(request.POST or None)
>        if form.is_valid():
>            form.save()
>            messages.add_message("It worked!")
>            return redirect('success')
>        ...
>
> If you're producing and consuming messages in the same request you
> might want a different tool, or you'll need to work around the
> assumptions that the messages framework makes.

Yeah. Some of these follow that pattern, and some produce and consume
in the same view. The messages framework seems to handle all of this
just fine. Perhaps I'm missing something, but my tests are passing and
the site is functioning as I expect.

Paul

Reply all
Reply to author
Forward
0 new messages