|
| 1 | +--- |
| 2 | +title: Opaque 500s on drawing-geometry and simulate from GenericMotor null fields, RocketPy ValueErrors and malformed ids |
| 3 | +date: 2026-10-18 |
| 4 | +category: runtime-errors |
| 5 | +module: services/motor, controllers/interface, repositories/interface, api |
| 6 | +problem_type: runtime_error |
| 7 | +component: service_object |
| 8 | +severity: critical |
| 9 | +symptoms: |
| 10 | + - "GET /motors/{id}/drawing-geometry, /rockets/{id}/drawing-geometry, /rockets/{id}/simulate and /flights/{id}/simulate return HTTP 500 'Unexpected error' for any GENERIC motor saved with chamber_position (or chamber_radius / chamber_height / propellant_initial_mass) = null" |
| 11 | + - "app.log: AttributeError: 'NoneType' object has no attribute 'shape' inside rocketpy motor.center_of_propellant_mass -> Function(self.chamber_position)" |
| 12 | + - "Liquid motor with an overfilled tank: RocketPy ValueError at MassFlowRateBasedTank construction surfaced as 500 instead of the message" |
| 13 | + - "PUT /rockets/undefined (jarvis client bug) -> bson InvalidId -> 500 'Unexpected error ocurred'" |
| 14 | + - "422 bodies were a bare JSON string ('RequestValidationError: [...]') so jarvis showed 'Unknown validation error'" |
| 15 | + - "jarvis Discord alerting escalated every one of these as P0 INTERNAL_SERVER_ERROR" |
| 16 | +root_cause: missing_validation |
| 17 | +resolution_type: code_fix |
| 18 | +related_components: |
| 19 | + - src/services/motor.py |
| 20 | + - src/controllers/interface.py |
| 21 | + - src/repositories/interface.py |
| 22 | + - src/api.py |
| 23 | + - rocketpy.GenericMotor |
| 24 | +tags: |
| 25 | + - rocketpy |
| 26 | + - generic-motor |
| 27 | + - error-handling |
| 28 | + - http-422 |
| 29 | + - validation |
| 30 | + - objectid |
| 31 | + - jarvis |
| 32 | +--- |
| 33 | + |
| 34 | +## Problem |
| 35 | + |
| 36 | +Between 2026-10-16 and 2026-10-18 the jarvis instrumentation channel received |
| 37 | +dozens of P0 alerts on `motor.drawingGeometry`, `rocket.drawingGeometry`, |
| 38 | +`rocket.simulate`, `flight.simulate` and `rocket.update`. Replaying the |
| 39 | +production entities (fetched from `api.rocketpy.org`) against a local |
| 40 | +Infinity showed four distinct faults, all answered with an opaque 500. |
| 41 | + |
| 42 | +1. **GenericMotor null fields.** `GenericMotor(chamber_position=None, ...)` |
| 43 | + constructs without complaint and only crashes when `center_of_propellant_mass` |
| 44 | + is first evaluated (`Function(None)` → `'NoneType' has no attribute 'shape'`). |
| 45 | + jarvis' COTS import fills chamber radius/height/propellant from the catalogue |
| 46 | + but never a chamber position, so every COTS-based GENERIC motor hit this. |
| 47 | +2. **RocketPy `ValueError` treated as a server fault.** An overfilled tank, a |
| 48 | + bad parachute trigger, inconsistent geometry — RocketPy raises `ValueError` |
| 49 | + with a good message; the controller wrapper turned it into 500. |
| 50 | +3. **Malformed ids.** `ObjectId("undefined")` raises `InvalidId`, which the |
| 51 | + repository wrapper turned into 500. |
| 52 | +4. **Non-standard 422 body.** The `RequestValidationError` handler returned |
| 53 | + `parse_error(exc)` (a string) as the whole body, so clients looking for |
| 54 | + `detail` found nothing. |
| 55 | + |
| 56 | +## Solution |
| 57 | + |
| 58 | +- `MotorService.from_motor_model`, GENERIC branch: collect |
| 59 | + `burn_time, chamber_radius, chamber_height, chamber_position, |
| 60 | + propellant_initial_mass`; if any is `None` raise |
| 61 | + `HTTPException(422, "Generic motor requires: a, b")`. Same "requires:" shape |
| 62 | + as the tank-kind guard so jarvis can paint field-level errors. |
| 63 | +- `controller_exception_handler`: `except ValueError` → 422 with the |
| 64 | + whitespace-collapsed message (logged at warning). Only genuine |
| 65 | + programming errors stay 500. |
| 66 | +- `repository_exception_handler`: let `HTTPException` through; |
| 67 | + `find_by_id` returns `None` for an invalid id (→ 404 upstream); |
| 68 | + `update_by_id` / `delete_by_id` raise 404 via `_object_id_or_404`. |
| 69 | +- `validation_exception_handler`: respond with FastAPI's standard |
| 70 | + `{"detail": [{"loc", "msg", "type"}]}` (dropping `input`/`ctx`, which is |
| 71 | + what made the original author stringify the error). |
| 72 | + |
| 73 | +Follow-up (same day): `RocketModel.motor` is now `Optional`. A rocket saved |
| 74 | +before its motor exists goes on the wire with `motor: null`, Infinity keeps the |
| 75 | +`EmptyMotor` rocketpy constructs every `Rocket` with (drawing already handled |
| 76 | +it), and `RocketService.require_motor()` answers 422 "Rocket has no motor yet" |
| 77 | +on rocket/flight simulate. jarvis sends `null` until `ids.motorId` is set, so |
| 78 | +the placeholder GENERIC motor never reaches Infinity any more. |
| 79 | + |
| 80 | +## Prevention |
| 81 | + |
| 82 | +- Any RocketPy constructor argument with no default must be validated at the |
| 83 | + API boundary; "constructs fine, crashes on first use" is the pattern to |
| 84 | + look for (RocketPy's lazy `Function` properties). |
| 85 | +- Keep `ValueError` → 422 in the controller wrapper; it is RocketPy's |
| 86 | + user-input error channel. |
| 87 | +- Run `python3 -I /Users/aasitvora/Desktop/chintu/regression-suite/run.py` |
| 88 | + before changing a model or service: it covers all 17 official designs and |
| 89 | + replays these incidents. |
0 commit comments