On Django 4.2 and below, formsURLField
adds the “http” scheme to scheme-less URLs. For example, it cleans example.com
into http://example.com
. Ticket 34380 makes this assumption https://example.com
.
In the related PR, the implementation initially went for a hard cut, changing the field to assume “https” immediately. But this was changed to a deprecation after this comment by David Wobrock, stating it was backwards-incompatible.
I propose moving the implementation back to a hard cut approach before Django 5.0 has its impending final release.
I base this proposal on my experience fixing the warning in a project that I was testing on Django 5.0. I started drafting a blog post covering the multiple required techniques to set assume_scheme
everywhere, covered at the end of this post. Whilst writing, I realized it’s a lot of work for such a minor change, and I think we can avoid hoisting this work on the community.
I would guess that most users entering a URL into a form copy-and-paste it from their browser, in which case it comes with a scheme. For the remaining cases where a URL comes with no scheme, it’s still overwhelmingly likely to work with HTTPS. According to Google Chrome’s statistics (“HTTPS Encryption by Chrome platform”), 93% of page loads on Windows use HTTPS.
Using the deprecation pathway to make this change adds warnings to projects for every URLField
, including those generated by ModelForm
, ModelAdmin
, and third-party packages. It helps only a small number of projects that rely on users entering HTTP URLs without schemes (corporate intranets are my prime suspect).
I think we can make a hard cut without much problem. As a precedent, the now-removed ping_google
command was hard cut to use HTTPS in Ticket 23829.
Sorry for the many words. Tagging people involved in the ticket: @felixxm.
Techniques from my drafted blog post:
Plain Forms with a URLField wrapper
For forms that you directly create, add the assume_scheme argument to their URLFields:
from django import forms
class CheckForm(forms.Form):
url = forms.URLField(label="URL to check", assume_scheme="https")
If you have many URLFields, try functools.partial to reduce the repetition:
from functools import partial
from django import forms
HTTPSURLField = partial(forms.URLField, assume_scheme="https")
class CheckForm(forms.Form):
url = HTTPSURLField(label="URL to check")
ModelForms with formfield_callback
ModelForms are a bit more tricky since their fields are automatically created “behind the scenes” from model fields. You can override these with explicit form fields, but that loses the automatic synchronization of other arguments like max_length.
Instead, you can use formfield_callback, added in Django 4.2. This callback hook allows you to wrap the creation of form fields to make adjustments as necessary. Use it to add assume_scheme=“https” to URLFields like so:
from django import forms
from django.db import models
from example.models import NicheMuseum
def urlfields_assume_https(db_field, **kwargs):
"""
ModelForm.Meta.formfield_callback function to assume HTTPS for scheme-less
domains in URLFields.
"""
if isinstance(db_field, models.URLField):
kwargs["assume_scheme"] = "https"
return db_field.formfield(**kwargs)
class NicheMuseumForm(forms.ModelForm):
class Meta:
model = NicheMuseum
fields = ["name", "website"]
formfield_callback = urlfields_assume_https
For forms that already use formfield_callback, you’ll need to merge functions as appropriate.
ModelAdmin form field method override
If you’re using Django’s admin site, that’s another source of outdated URLFields. ModelAdmin classes automatically generate ModelForm classes, unless a form is explicitly declared. Because this form class is created per-request, the warning won’t appear at import time, but when you load the related “add” or “change” pages:
$ python -Xdev manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
...
/.../django/db/models/fields/__init__.py:1140: RemovedInDjango60Warning: The default scheme will be changed from 'http' to 'https' in Django 6.0. Pass the forms.URLField.assume_scheme argument to silence this warning.
return form_class(**defaults)
[25/Nov/2023 16:37:34] "GET /admin/example/nichemuseum/add/ HTTP/1.1" 200 8907
...
You can also expose the warnings in your test suite by testing all of your ModelAdmin classes. See my previous post on using parameterized tests to do this easily.
The easiest way to fix these warnings is with the undocumented ModelAdmin.formfield_for_dbfield() method. This is passed as ModelForm.formfield_callback, and applies some customizations for the admin site. Override it in custom admin classes that you then use as bases in your project:
from django.contrib import admin
from django.contrib.admin.options import BaseModelAdmin
from django.db import models
from example.models import NicheMuseum
# Custom admin classes for project-level modifications
class AdminMixin(BaseModelAdmin):
def formfield_for_dbfield(self, db_field, request, **kwargs):
"""
Assume HTTPS for scheme-less domains pasted into URLFields.
"""
if isinstance(db_field, models.URLField):
kwargs["assume_scheme"] = "https"
return super().formfield_for_dbfield(db_field, request, **kwargs)
class ModelAdmin(AdminMixin, admin.ModelAdmin):
pass
class StackedInline(AdminMixin, admin.StackedInline):
pass
class TabularInline(AdminMixin, admin.TabularInline):
pass
# Admin classes
@admin.register(NicheMuseum)
class NicheMuseumAdmin(ModelAdmin):
pass
Third-party packages
URLFields in third-party packages may be hard to fix in your project. Some packages do allow you to replace their form classes, so you can use the above techniques. Others don’t, so you may need to resort to monkey-patching.
In either case, projects will hopefully accept pull requests to use assume_scheme=“https”, if appropriate. Packages can pass the argument on Django 5.0+ by using this pattern:
from functools import partial
import django
from django import forms
if django.VERSION >= (5, 0):
URLField = partial(forms.URLField, assume_scheme="https")
else:
URLField = forms.URLField
class CheckForm(forms.Form):
url = URLField(label="URL to check")