Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django QueryDict empty with request.POST but populated in request.GET

Short version: On a django site, I can grab values from request.GET but not request.POST in response to a request from Twilio. I suspect it has something to do with csrf, but I'm not really sure how to debug the problem. Details below.

Long version: I am helping a friend with a project where we are running a medical survey over SMS using the Twilio REST API. I had a domain and a very bare-bones django-built site on that domain, which I had built really just to better familiarize myself with django, so we're using that.

We're collecting SMS responses to our survey and as part of the Twilio API, it sends any response to our number to a url specified under the account, so we have the response targeting something like the following:

...mydomain.com/some_page/another_page/

The Twilio request then looks something like the following:

...mydomain.com/some_page/another_page/?AccountSid=###SOME_LONG_ACCOUNT_SIDE&From=%2BPHONE_NUMBER&Body=bla+BLA+bla+BLA&SmsSid=##MESSAGE_ID_KEY&SmsMessageSid=##MESSAGE_ID_KEY&FromCity=Santa+Cruz&FromState=California...

Working Code

I am testing that the incoming request has our AccountSid inside it (compared with the value in the database) and in my views.py for the app, I have something that looks like the following (and this works):

from our_app import TwilioAccount
our_account = TwilioAccount.objects.get(id=1)

def twilio_response(request):
    assert request.GET.get('AccountSid', None) == our_account.account_sid
    ## log the incoming request to the database under survey responses...

Non-Working Code

If I log-in to our Twilio account and switch the request method to POST, and then I switch all of my data collecting to request.POST, the above assert statement fails. Further debugging reveals that my QueryDict is empty under POST, POST: {}, so there is no key value grabbed.

I thought this maybe was because POST under django requires a csrf_token, but I figured checking for the AccountSid was fairly good, so I imported csrf_exempt and wrapped the above function with that:

@csrf_exempt
def twilio_response(request):
    assert request.POST.get('AccountSid', None) == our_account.account_sid
    ## log the incoming request to the database under survey responses...

AssertionError: ...

This does not work with the exact same request: the QueryDict is empty.


Questions:

1) Is there something else I need to do to make my @csrf_exempt work? Alternate question: is this a terrible and dumb way to do this? How do people usually satisfy this requirement when working with other company's APIs and not actual, logged-in users?

1a) Instead of making it csrf_exempt, I could just keep it as a GET request, knowing that it's still checking all incoming requests against our account_sid. Should I do that or is that a really naive way to do it?

2) I am eager to learn the best way to do this: should I build a django form and then route the request to my form and test validity and clean the data that way? If so, can anyone give me a loose outline on what the view/form would look like (complete with csrf_token) when there's not going to be a template for the form?

like image 818
erewok Avatar asked Aug 12 '13 23:08

erewok


1 Answers

Matt from the Twilio Developer Evangelist team here.

1) Wrapping your twilio_response function with @csrf_exempt to remove the Django CSRF token check is the right way to go here. Twilio does not generate a Django CSRF token. Instead, there are other ways to validate POSTs are coming from Twilio, such as signature validation with the X-Twilio-Signature header. See the Twilio security docs for details.

1a) Using the GET request is convenient for testing and debugging, but POST should be used in production. GET requests do not have a body according to the HTTP spec, so the results are passed in the query string. If the parameters are too large, for example with a text message that has a maximum length of 1600 characters, the query string in the URL could exceed the maximum length of a URL and potentially cause issues when you handle the string.

2) Django forms are a good way to go for this use case, particularly a ModelForm that leverages your existing Model used to save the response. For example, your ModelForm could look something like the following if you are saving your data to a TwilioMessage model:

from django.forms import ModelForm
from .models import TwilioMessage


class MessageForm(ModelForm):
    pass

    class Meta:
        model = ReactionEvent
        # include all fields you're saving from the form here
        fields = ['body', 'to', 'from_', 'signature',] 
like image 115
Matt Avatar answered Oct 14 '22 03:10

Matt