|
| 1 | +# DRF Issue #6855 Concept Location |
| 2 | + |
| 3 | +## Problem Summary |
| 4 | + |
| 5 | +The browsable API can crash while rendering an extra action page when the response data comes from a serializer whose `instance` is not a normal model object for the current view's permission class. |
| 6 | + |
| 7 | +The failing path is in the browsable API renderer, not in view dispatch: |
| 8 | + |
| 9 | +1. The extra action returns `Response(serializer.data)`. |
| 10 | +2. `serializer.data` is a `ReturnDict` or `ReturnList` that keeps a backlink to the serializer. |
| 11 | +3. `BrowsableAPIRenderer` inspects that serializer during HTML rendering. |
| 12 | +4. While building placeholder UI for other HTTP methods, it calls `view.check_object_permissions(request, instance)`. |
| 13 | +5. `instance` may be a non-model object from the serializer, which can be incompatible with the permission class and crash with `AttributeError`. |
| 14 | + |
| 15 | +## Primary Source Locations |
| 16 | + |
| 17 | +### `rest_framework/renderers.py` |
| 18 | + |
| 19 | +This is the main issue location. |
| 20 | + |
| 21 | +- `BrowsableAPIRenderer.get_context()` |
| 22 | + - Always asks for `put_form`, `post_form`, `delete_form`, and `options_form`. |
| 23 | + - This means a normal `GET` render also evaluates synthetic UI for other methods. |
| 24 | + |
| 25 | +- `BrowsableAPIRenderer.get_rendered_html_form()` |
| 26 | + - Pulls `serializer = getattr(data, 'serializer', None)`. |
| 27 | + - Extracts `instance = getattr(serializer, 'instance', None)` when `many=False`. |
| 28 | + - Calls `show_form_for_method()` before the current `DELETE` / `OPTIONS` bailout. |
| 29 | + - This ordering is what exposes the bug. |
| 30 | + |
| 31 | +- `BrowsableAPIRenderer.show_form_for_method()` |
| 32 | + - Calls `view.check_permissions(request)`. |
| 33 | + - If `obj is not None`, also calls `view.check_object_permissions(request, obj)`. |
| 34 | + - Only catches `APIException`, so an unexpected `AttributeError` from a permission class bubbles up and crashes rendering. |
| 35 | + |
| 36 | +## Supporting Locations |
| 37 | + |
| 38 | +### `rest_framework/utils/serializer_helpers.py` |
| 39 | + |
| 40 | +- `ReturnDict` |
| 41 | +- `ReturnList` |
| 42 | + |
| 43 | +These wrappers preserve `data.serializer`, which is why renderers can see the original serializer and its `.instance`. |
| 44 | + |
| 45 | +### `rest_framework/views.py` |
| 46 | + |
| 47 | +- `APIView.check_object_permissions()` |
| 48 | + |
| 49 | +This loops through permission classes and directly passes the provided object to `has_object_permission()`. It assumes the caller supplied the correct domain object. |
| 50 | + |
| 51 | +### `rest_framework/request.py` |
| 52 | + |
| 53 | +- `override_method` |
| 54 | + |
| 55 | +`get_rendered_html_form()` uses this context manager while probing alternate HTTP methods such as `OPTIONS` and `DELETE`. |
| 56 | + |
| 57 | +## UI/Template Locations |
| 58 | + |
| 59 | +### `rest_framework/templates/rest_framework/base.html` |
| 60 | + |
| 61 | +- `options_form` |
| 62 | +- `delete_form` |
| 63 | + |
| 64 | +These booleans control whether the browsable API shows the `OPTIONS` button and `DELETE` button/modal. |
| 65 | + |
| 66 | +### `rest_framework/templates/rest_framework/admin.html` |
| 67 | + |
| 68 | +- `delete_form` |
| 69 | + |
| 70 | +The admin renderer also consumes the truthiness of `delete_form`. |
| 71 | + |
| 72 | +## Existing Test Locations To Extend |
| 73 | + |
| 74 | +### `tests/test_renderers.py` |
| 75 | + |
| 76 | +Contains `BrowsableAPIRendererTests`, which is the closest existing unit/integration coverage for renderer behavior and extra actions. |
| 77 | + |
| 78 | +### `tests/browsable_api/test_form_rendering.py` |
| 79 | + |
| 80 | +Already has regression coverage around browsable API form generation and serializer shapes, including `many=True`. |
| 81 | + |
| 82 | +### `tests/browsable_api/views.py` |
| 83 | + |
| 84 | +Contains an example object-permission class that accesses nested attributes on the object and is useful as a pattern for reproducing this class of failure. |
| 85 | + |
| 86 | +## Key Conceptual Insight |
| 87 | + |
| 88 | +The renderer is mixing two different concepts: |
| 89 | + |
| 90 | +- the object that was serialized for display |
| 91 | +- the object that should be used for permission checks when deciding whether to show action UI |
| 92 | + |
| 93 | +For issue #6855, the immediate crash is triggered by the synthetic `OPTIONS` UI path, where no object-level edit form is actually rendered, so reusing `serializer.instance` is not appropriate. |
0 commit comments