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?
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',]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With