Vor kurzem war ich beauftragt einen Bug in einer Django Applikation zu finden, der scheinbar rein zufällig bei einem Formular auftrat. Das Form hatte ein Feld "description" dass scheinbar zufällig ab und zu einen falschen Wert anzeigte.
Fehlersuche
Der Fehler lies sich aber vorherst nur in der Produktivumgebung reproduzieren, und dass aber auch nicht verlässlich. Die falschen Werte schienen aus einem anderen Model zu kommen, dass ebenfalls ein "description" Feld hatte.
Das betroffene Form selbst hatte keine Überschreibungen und auch der verwendete View war ziemlich Basic:
class ProductForm(forms.ModelForm):
class Meta:
fields = (
'sku','batch', 'name', 'description' )
model = Product
class AddMaterialView(LoginRequiredMixin, FormView):
template_name = 'production/add_material.html'
form_class = ProductForm
def get_context_data(self, **kwargs):
order = Order.objects.get( pk=self.kwargs.get('order_id') )
kwargs['order'] = order
return super().get_context_data(**kwargs)
def form_valid(self, form):
kwargs={'order_id': self.kwargs.get('order_id')}
batch = form.cleaned_data.get('batch', False)
if batch:
...
else:
...
self.success_url = reverse_lazy(next, kwargs=kwargs)
return super().form_valid(form)
Da der Fehler nicht immer sondern nur ab und zu auftrat, aber auch die falschen Werte sich zu wiederholen schienen, lag der Verdacht nahe dass das Auftreten bzw nicht Auftreten des Fehlers an einem Thread oder Prozess festgemacht werden kann.
Doch was genau war der Auslöser der einen Thread vergiftete?
Die Produktivumgebung lief in Docker Containern mit gunicorn und jeweils 12 Workers mit jeweils 8 Threads. Mangels Debuggingmöglichkeiten baute ich einen Debug Version des Formulars um mir verschiedene Werte ausgeben zu können. Mein Ursprungsverdacht war ein Caching Problem entweder im Template oder den Formfields, dass ich aber mit meinem Debugview wiederlegen konnte.
Da das Formular die richtige Instanz mit dem richtigen Wert für "description" erhielt, der Initialwert des Fields bzw des Fieldwidets aber falsch war, musste der Fehler beim Setzen des Initialwerts liegen. Das Problem: Die Form ist wie oben zu sehen eine Standard Model Form. Ich las mich noch durch den Django Code zur Initialisierung der ModelForm aber dass brachte mich nicht weiter.
Der View ist das Problem
Da man nicht genau benennen konnte seit wann dieser Bug auftrag, aber ihn vor 10 Tagen das erste Mal bemerkte durchforstete ich die git commits nach etwas verdächtigem.
Ich wurde in einem View einer anderen App fündig:
class SomeNotRelatedView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
form_class = forms.SomeForm
def get_context_data(self, **kwargs):
order_id=self.kwargs.get('order_id')
...
self.initial['description'] = order.description
...
Ein Entwickler hatte hier versucht im get_context_data des Views ein Initial Value zu setzen. Stattdessen wurde der Initialvalue für alle Views damit gesetzt, da nicht wie man annehmen würde self.initial['description'] die Instanzvariable des Views ändert, und auch nicht der Klasse, sondern von der geerbten! Klasse Formview.
Damit konnte ich das Problem lokal nachstellen, da ich vorher einfach diesen View aufrufen musste um den aktuellen Thread zu vergiften.
Als Probe testete ich verschiedene andere Views, die nun alle einen Initialwert für 'description' hatten.
der Fix
Der Fix war die dafür vorhergesehene Methode get_initial zu verwenden um den Initialwert zu setzen:
def get_initial(self):
"""
use get_initial to set initial values.
Dont set self.initial[] as this will override initial values for all views!
@return:
"""
order_id = self.kwargs.get('order_id')
order = Order.objects.get(id=order_id)
initial = super().get_initial()
....
initial['description'] = order.description
...
return initial