HTTP methods define the type of action being performed on a resource. Django provides comprehensive support for handling different HTTP methods, enabling you to build RESTful APIs and implement proper request handling patterns.
from django.views.decorators.http import require_http_methods, require_GET, require_POST, require_safe
from django.http import JsonResponse, HttpResponseNotAllowed
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
# GET - Retrieve data (safe, idempotent)
@require_GET
def get_posts(request):
"""Retrieve list of posts"""
posts = Post.objects.filter(status='published').order_by('-created_at')
# Handle query parameters
search = request.GET.get('search', '')
category = request.GET.get('category')
page = int(request.GET.get('page', 1))
if search:
posts = posts.filter(title__icontains=search)
if category:
posts = posts.filter(category__slug=category)
# Pagination
from django.core.paginator import Paginator
paginator = Paginator(posts, 10)
posts_page = paginator.get_page(page)
return render(request, 'blog/post_list.html', {
'posts': posts_page,
'search': search,
'category': category
})
# POST - Create new resource
@require_POST
@login_required
def create_post(request):
"""Create new post"""
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m()
return JsonResponse({
'success': True,
'post_id': post.id,
'redirect_url': post.get_absolute_url()
})
else:
return JsonResponse({
'success': False,
'errors': form.errors
}, status=400)
# PUT - Update entire resource
@require_http_methods(["PUT"])
@login_required
def update_post(request, pk):
"""Update entire post"""
post = get_object_or_404(Post, pk=pk)
# Check permissions
if post.author != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
try:
import json
data = json.loads(request.body)
# Update all fields
post.title = data.get('title', post.title)
post.content = data.get('content', post.content)
post.status = data.get('status', post.status)
# Validate and save
post.full_clean()
post.save()
return JsonResponse({
'success': True,
'message': 'Post updated successfully'
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
except ValidationError as e:
return JsonResponse({'error': str(e)}, status=400)
# PATCH - Partial update
@require_http_methods(["PATCH"])
@login_required
def partial_update_post(request, pk):
"""Partially update post"""
post = get_object_or_404(Post, pk=pk)
if post.author != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
try:
import json
data = json.loads(request.body)
# Update only provided fields
updated_fields = []
if 'title' in data:
post.title = data['title']
updated_fields.append('title')
if 'content' in data:
post.content = data['content']
updated_fields.append('content')
if 'status' in data:
post.status = data['status']
updated_fields.append('status')
if updated_fields:
post.save(update_fields=updated_fields)
return JsonResponse({
'success': True,
'updated_fields': updated_fields
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
# DELETE - Remove resource
@require_http_methods(["DELETE"])
@login_required
def delete_post(request, pk):
"""Delete post"""
post = get_object_or_404(Post, pk=pk)
if post.author != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
post.delete()
return JsonResponse({
'success': True,
'message': 'Post deleted successfully'
}, status=204)
# HEAD - Get headers only (like GET but no body)
@require_http_methods(["HEAD", "GET"])
def post_head(request, pk):
"""Handle HEAD requests for post metadata"""
post = get_object_or_404(Post, pk=pk, status='published')
response = HttpResponse()
response['Content-Type'] = 'text/html'
response['Content-Length'] = len(post.content)
response['Last-Modified'] = post.updated_at.strftime('%a, %d %b %Y %H:%M:%S GMT')
response['ETag'] = f'"{post.id}-{post.updated_at.timestamp()}"'
if request.method == 'GET':
# Return full content for GET
return render(request, 'blog/post_detail.html', {'post': post})
# Return empty response for HEAD
return response
# OPTIONS - Get allowed methods
@require_http_methods(["OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"])
def post_options(request, pk=None):
"""Handle OPTIONS requests"""
if request.method == 'OPTIONS':
response = HttpResponse()
if pk:
# Specific resource
response['Allow'] = 'GET, PUT, PATCH, DELETE, HEAD, OPTIONS'
else:
# Collection
response['Allow'] = 'GET, POST, HEAD, OPTIONS'
response['Access-Control-Allow-Methods'] = response['Allow']
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response
# Handle other methods...
return JsonResponse({'message': 'Method handling here'})
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
import json
@csrf_exempt
def post_api_view(request, pk=None):
"""RESTful API endpoint for posts"""
if request.method == 'GET':
if pk:
# GET /api/posts/1/ - Retrieve specific post
post = get_object_or_404(Post, pk=pk, status='published')
data = {
'id': post.id,
'title': post.title,
'content': post.content,
'author': {
'id': post.author.id,
'username': post.author.username
},
'created_at': post.created_at.isoformat(),
'updated_at': post.updated_at.isoformat()
}
return JsonResponse(data)
else:
# GET /api/posts/ - List posts
posts = Post.objects.filter(status='published').select_related('author')
# Pagination
page = int(request.GET.get('page', 1))
per_page = min(int(request.GET.get('per_page', 10)), 100)
from django.core.paginator import Paginator
paginator = Paginator(posts, per_page)
posts_page = paginator.get_page(page)
data = {
'posts': [
{
'id': post.id,
'title': post.title,
'author': post.author.username,
'created_at': post.created_at.isoformat()
}
for post in posts_page.object_list
],
'pagination': {
'current_page': posts_page.number,
'total_pages': paginator.num_pages,
'total_count': paginator.count,
'has_next': posts_page.has_next(),
'has_previous': posts_page.has_previous()
}
}
return JsonResponse(data)
elif request.method == 'POST':
# POST /api/posts/ - Create new post
if not request.user.is_authenticated:
return JsonResponse({'error': 'Authentication required'}, status=401)
try:
data = json.loads(request.body)
# Validate required fields
if not data.get('title') or not data.get('content'):
return JsonResponse({
'error': 'Title and content are required'
}, status=400)
# Create post
post = Post.objects.create(
title=data['title'],
content=data['content'],
author=request.user,
status=data.get('status', 'draft')
)
return JsonResponse({
'id': post.id,
'title': post.title,
'created_at': post.created_at.isoformat()
}, status=201)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
elif request.method == 'PUT':
# PUT /api/posts/1/ - Update entire post
if not pk:
return JsonResponse({'error': 'Post ID required for PUT'}, status=400)
post = get_object_or_404(Post, pk=pk)
if post.author != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
try:
data = json.loads(request.body)
# Update all fields
post.title = data.get('title', '')
post.content = data.get('content', '')
post.status = data.get('status', 'draft')
post.full_clean()
post.save()
return JsonResponse({
'id': post.id,
'title': post.title,
'updated_at': post.updated_at.isoformat()
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
except ValidationError as e:
return JsonResponse({'error': str(e)}, status=400)
elif request.method == 'PATCH':
# PATCH /api/posts/1/ - Partial update
if not pk:
return JsonResponse({'error': 'Post ID required for PATCH'}, status=400)
post = get_object_or_404(Post, pk=pk)
if post.author != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
try:
data = json.loads(request.body)
updated_fields = []
for field in ['title', 'content', 'status']:
if field in data:
setattr(post, field, data[field])
updated_fields.append(field)
if updated_fields:
post.save(update_fields=updated_fields)
return JsonResponse({
'id': post.id,
'updated_fields': updated_fields,
'updated_at': post.updated_at.isoformat()
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
elif request.method == 'DELETE':
# DELETE /api/posts/1/ - Delete post
if not pk:
return JsonResponse({'error': 'Post ID required for DELETE'}, status=400)
post = get_object_or_404(Post, pk=pk)
if post.author != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
post.delete()
return JsonResponse({'message': 'Post deleted successfully'}, status=204)
else:
# Method not allowed
return HttpResponseNotAllowed(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
@require_http_methods(["GET", "POST"])
def post_form_view(request, pk=None):
"""Handle both display and submission of post form"""
# Determine if this is create or edit
if pk:
post = get_object_or_404(Post, pk=pk)
# Check edit permissions
if post.author != request.user and not request.user.is_staff:
return HttpResponseForbidden("You don't have permission to edit this post")
else:
post = None
if request.method == 'GET':
# Display form
if post:
form = PostForm(instance=post)
template_context = {
'form': form,
'post': post,
'is_edit': True
}
else:
form = PostForm()
template_context = {
'form': form,
'is_edit': False
}
return render(request, 'blog/post_form.html', template_context)
elif request.method == 'POST':
# Process form submission
if post:
form = PostForm(request.POST, request.FILES, instance=post)
else:
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
if not post.author_id:
post.author = request.user
# Handle different submit buttons
if 'save_draft' in request.POST:
post.status = 'draft'
messages.success(request, 'Post saved as draft.')
elif 'publish' in request.POST:
post.status = 'published'
messages.success(request, 'Post published successfully!')
elif 'save_continue' in request.POST:
post.save()
form.save_m2m()
messages.success(request, 'Post saved. Continue editing.')
return redirect('blog:edit_post', pk=post.pk)
post.save()
form.save_m2m()
return redirect('blog:post_detail', pk=post.pk)
else:
# Form has errors
messages.error(request, 'Please correct the errors below.')
template_context = {
'form': form,
'post': post,
'is_edit': bool(post)
}
return render(request, 'blog/post_form.html', template_context)
@require_http_methods(["POST", "DELETE"])
@login_required
def post_delete_view(request, pk):
"""Handle post deletion with confirmation"""
post = get_object_or_404(Post, pk=pk)
if post.author != request.user and not request.user.is_staff:
return HttpResponseForbidden("You don't have permission to delete this post")
if request.method == 'POST':
# Show confirmation page
return render(request, 'blog/post_confirm_delete.html', {'post': post})
elif request.method == 'DELETE':
# Actually delete the post
post_title = post.title
post.delete()
# Handle different response formats
if request.headers.get('Accept') == 'application/json':
return JsonResponse({
'success': True,
'message': f'Post "{post_title}" deleted successfully'
})
else:
messages.success(request, f'Post "{post_title}" deleted successfully.')
return redirect('blog:post_list')
from functools import wraps
from django.http import HttpResponseNotAllowed, JsonResponse
def allow_methods(*methods):
"""Custom decorator to allow specific HTTP methods"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if request.method not in methods:
if request.headers.get('Accept') == 'application/json':
return JsonResponse({
'error': f'Method {request.method} not allowed',
'allowed_methods': list(methods)
}, status=405)
else:
return HttpResponseNotAllowed(methods)
return view_func(request, *args, **kwargs)
# Set allowed methods for OPTIONS handling
wrapper.allowed_methods = methods
return wrapper
return decorator
def method_dispatcher(**method_handlers):
"""Decorator to dispatch different methods to different functions"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
method = request.method.lower()
if method in method_handlers:
return method_handlers[method](request, *args, **kwargs)
else:
# Fall back to original view function
return view_func(request, *args, **kwargs)
return wrapper
return decorator
# Usage examples
@allow_methods('GET', 'POST', 'PUT', 'DELETE')
def api_endpoint(request):
"""API endpoint with custom method validation"""
if request.method == 'GET':
return JsonResponse({'message': 'GET request'})
elif request.method == 'POST':
return JsonResponse({'message': 'POST request'})
# ... handle other methods
def handle_get(request, *args, **kwargs):
return JsonResponse({'method': 'GET'})
def handle_post(request, *args, **kwargs):
return JsonResponse({'method': 'POST'})
def handle_put(request, *args, **kwargs):
return JsonResponse({'method': 'PUT'})
@method_dispatcher(
get=handle_get,
post=handle_post,
put=handle_put
)
def dispatched_view(request):
"""Default handler for unspecified methods"""
return JsonResponse({'method': 'DEFAULT'})
import json
from django.http import JsonResponse, HttpResponseBadRequest
def handle_content_types(request):
"""Handle different content types in requests"""
if request.method == 'POST':
content_type = request.content_type.lower()
if content_type == 'application/json':
# Handle JSON data
try:
data = json.loads(request.body)
return JsonResponse({
'message': 'JSON data received',
'data': data
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
elif content_type == 'application/x-www-form-urlencoded':
# Handle form data
data = dict(request.POST)
return JsonResponse({
'message': 'Form data received',
'data': data
})
elif content_type.startswith('multipart/form-data'):
# Handle multipart form (with files)
files = {name: file.name for name, file in request.FILES.items()}
data = dict(request.POST)
return JsonResponse({
'message': 'Multipart data received',
'data': data,
'files': files
})
elif content_type == 'text/plain':
# Handle plain text
text_data = request.body.decode('utf-8')
return JsonResponse({
'message': 'Text data received',
'data': text_data
})
elif content_type == 'application/xml' or content_type == 'text/xml':
# Handle XML data
xml_data = request.body.decode('utf-8')
return JsonResponse({
'message': 'XML data received',
'data': xml_data
})
else:
return JsonResponse({
'error': f'Unsupported content type: {content_type}'
}, status=415)
return JsonResponse({'message': 'Send POST request with data'})
def flexible_api_endpoint(request):
"""API endpoint that handles multiple content types and methods"""
def parse_request_data():
"""Parse request data based on content type"""
if request.method in ['POST', 'PUT', 'PATCH']:
content_type = request.content_type.lower()
if 'application/json' in content_type:
return json.loads(request.body)
elif 'application/x-www-form-urlencoded' in content_type:
return dict(request.POST)
elif 'multipart/form-data' in content_type:
data = dict(request.POST)
data.update({name: file for name, file in request.FILES.items()})
return data
return {}
try:
data = parse_request_data()
if request.method == 'GET':
return JsonResponse({'message': 'GET request', 'params': dict(request.GET)})
elif request.method == 'POST':
# Create resource
return JsonResponse({'message': 'Created', 'data': data}, status=201)
elif request.method == 'PUT':
# Update resource
return JsonResponse({'message': 'Updated', 'data': data})
elif request.method == 'PATCH':
# Partial update
return JsonResponse({'message': 'Partially updated', 'data': data})
elif request.method == 'DELETE':
# Delete resource
return JsonResponse({'message': 'Deleted'}, status=204)
else:
return HttpResponseNotAllowed(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def cors_enabled_api(request):
"""API endpoint with CORS support"""
# Handle preflight OPTIONS request
if request.method == 'OPTIONS':
response = HttpResponse()
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'
response['Access-Control-Max-Age'] = '86400' # 24 hours
return response
# Handle actual request
if request.method == 'GET':
data = {'message': 'CORS-enabled GET response'}
elif request.method == 'POST':
data = {'message': 'CORS-enabled POST response'}
else:
data = {'message': f'CORS-enabled {request.method} response'}
response = JsonResponse(data)
# Add CORS headers to actual response
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Credentials'] = 'true'
return response
def cors_middleware_example(get_response):
"""Example CORS middleware"""
def middleware(request):
response = get_response(request)
# Add CORS headers to all responses
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response
return middleware
# tests/test_http_methods.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
import json
class HTTPMethodTests(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass'
)
self.post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)
def test_get_request(self):
"""Test GET request handling"""
response = self.client.get(f'/api/posts/{self.post.pk}/')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['title'], 'Test Post')
def test_post_request(self):
"""Test POST request handling"""
self.client.login(username='testuser', password='testpass')
post_data = {
'title': 'New Post',
'content': 'New content'
}
response = self.client.post('/api/posts/',
json.dumps(post_data),
content_type='application/json')
self.assertEqual(response.status_code, 201)
data = response.json()
self.assertEqual(data['title'], 'New Post')
def test_put_request(self):
"""Test PUT request handling"""
self.client.login(username='testuser', password='testpass')
update_data = {
'title': 'Updated Post',
'content': 'Updated content'
}
response = self.client.put(f'/api/posts/{self.post.pk}/',
json.dumps(update_data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
# Verify update
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Updated Post')
def test_patch_request(self):
"""Test PATCH request handling"""
self.client.login(username='testuser', password='testpass')
patch_data = {'title': 'Patched Title'}
response = self.client.patch(f'/api/posts/{self.post.pk}/',
json.dumps(patch_data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
# Verify partial update
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Patched Title')
self.assertEqual(self.post.content, 'Test content') # Unchanged
def test_delete_request(self):
"""Test DELETE request handling"""
self.client.login(username='testuser', password='testpass')
response = self.client.delete(f'/api/posts/{self.post.pk}/')
self.assertEqual(response.status_code, 204)
# Verify deletion
self.assertFalse(Post.objects.filter(pk=self.post.pk).exists())
def test_options_request(self):
"""Test OPTIONS request handling"""
response = self.client.options(f'/api/posts/{self.post.pk}/')
self.assertEqual(response.status_code, 200)
self.assertIn('Allow', response)
self.assertIn('GET', response['Allow'])
def test_method_not_allowed(self):
"""Test method not allowed handling"""
# Try TRACE method (not typically allowed)
response = self.client.trace('/api/posts/')
self.assertEqual(response.status_code, 405)
def test_content_type_handling(self):
"""Test different content types"""
self.client.login(username='testuser', password='testpass')
# JSON content type
json_data = {'title': 'JSON Post', 'content': 'JSON content'}
response = self.client.post('/api/posts/',
json.dumps(json_data),
content_type='application/json')
self.assertEqual(response.status_code, 201)
# Form content type
form_data = {'title': 'Form Post', 'content': 'Form content'}
response = self.client.post('/api/posts/', form_data)
self.assertEqual(response.status_code, 201)
Proper HTTP method handling is essential for building RESTful APIs and web applications that follow HTTP standards. Understanding when and how to use different methods enables you to create intuitive, predictable interfaces that work well with various clients and tools.
Redirects
Redirects are essential for guiding users through your application, handling URL changes, and implementing proper navigation flows. Django provides multiple ways to handle redirects with different HTTP status codes and use cases.
Conditional View Processing
Conditional view processing allows Django to handle HTTP conditional requests efficiently, reducing bandwidth and improving performance by serving cached content when appropriate. This includes ETags, Last-Modified headers, and conditional GET/HEAD requests.