Skip to content

Commit 7c15f6e

Browse files
authored
Rescue bike attribute assignment instead of 500ing (#4036)
A form filling bot POSTs `/bikes` with every field set to `"1"`. `frame_material: "1"` isn't a valid enum value, and assigning it raised `ArgumentError` — [1,441 notices in 7 days](https://app.honeybadger.io/projects/35931/faults/133165943), plus [a second fault](https://app.honeybadger.io/projects/35931/faults/133168206) on the redirect back, where `embed_extended` rebuilds the form from the same stored b_param and raises again. - **`BikeServices::Builder.build` assigns the bike's attributes one at a time and rescues**, adding an error on whichever attribute the model refused. That's the whole fix — nothing about enums specifically, so anything else assignment can raise on lands the same way. Assigning individually is what keeps the rest of the attributes on the bike, so the re-rendered form still holds what was entered. - The bike is then invalid through the existing `bike.errors` → `b_param.bike_errors` path, which every entry point already handles: `/bikes` redirects to the form, the embed flashes the message, and the registration flow returns to step 2. Specs cover the two faulted paths plus the registration flow, which permits no enum a bot can poison (`cycle_type` is `friendly_find`ed, `status` is gated by `BParam#status`) but still builds the bike from whatever the registration holds.
1 parent c277172 commit 7c15f6e

5 files changed

Lines changed: 59 additions & 1 deletion

File tree

app/services/bike_services/builder.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ def build(b_param, new_attrs = nil)
1010
bike = Bike.new(cycle_type: "bike")
1111
# passed_organization is assigned unless b_param has an organization
1212
passed_organization = new_attrs.delete(:organization)
13-
bike.attributes = b_param.safe_bike_attrs(new_attrs)
13+
# One at a time, so a value the model refuses to take (an out of range enum, say)
14+
# becomes an error on the bike rather than a 500, and the rest still land
15+
b_param.safe_bike_attrs(new_attrs).each do |key, value|
16+
bike.attributes = {key => value}
17+
rescue ArgumentError
18+
bike.errors.add(key, "is not valid")
19+
end
1420

1521
bike.address_record&.bike = bike # Kinda gross, but gotta get it both ways!
1622

spec/requests/bikes/create_request_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ def expect_created_stolen_bike(bike_params: nil, stolen_params: {})
229229
end
230230
end
231231
end
232+
context "invalid frame_material" do
233+
# Form filling bots submit "1" for every field - the enum assignment used to raise
234+
let(:bike_params) { basic_bike_params.merge(frame_material: "1") }
235+
it "renders the error" do
236+
expect {
237+
post base_url, params: {bike: bike_params}
238+
}.to change(Bike, :count).by(0)
239+
b_param = BParam.last
240+
expect(b_param.bike_errors).to eq(["Frame material is not valid"])
241+
expect(response).to redirect_to(new_bike_url(b_param_token: b_param.id_token))
242+
end
243+
end
232244
context "no existing b_param, impounded" do
233245
let(:bike_params) { basic_bike_params }
234246
context "impound_record" do

spec/requests/organizations_request_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,18 @@
231231
expect(bike.status).to eq "status_stolen"
232232
expect(bike.owner_email).to eq(b_param_attrs[:bike][:owner_email])
233233
end
234+
context "with an invalid enum value" do
235+
let(:b_param_attrs) do
236+
{bike: {owner_email: "someemail@stuff.com", frame_material: "1",
237+
creation_organization_id: current_organization.id.to_s}}
238+
end
239+
it "renders" do
240+
get "#{base_url}/#{current_organization.id}/embed_extended?b_param_id_token=#{b_param.id_token}"
241+
expect(response.code).to eq("200")
242+
expect(response).to render_template(:embed_extended)
243+
expect(assigns(:bike).frame_material).to be_blank
244+
end
245+
end
234246
end
235247
end
236248

spec/requests/register_request_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,24 @@ def address_street_field
702702
end
703703
end
704704

705+
# This flow permits no enum a bot could poison - cycle_type is friendly_found and
706+
# status is gated by BParam#status - but it creates the bike from whatever the
707+
# registration already holds, so a bad value can still arrive at Builder
708+
context "registration holding an invalid enum value" do
709+
let(:b_param) do
710+
BParam.create(origin: "register_flow",
711+
params: {bike: {owner_email:, manufacturer_id: "Trek", frame_material: "1"}}.as_json)
712+
end
713+
714+
it "sends them back to step 2 with the error, rather than raising" do
715+
expect {
716+
patch base_url, params: {b_param_token: b_param.id_token, bike: bike_details}
717+
}.to_not change(Bike, :count)
718+
expect(flash[:error]).to match(/frame material/i)
719+
expect(response).to redirect_to register_path(b_param_token: b_param.id_token, step: 2)
720+
end
721+
end
722+
705723
context "registering to their own address" do
706724
let(:owner_email) { current_user.email }
707725

spec/services/bike_services/builder_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@
127127
end
128128
end
129129

130+
context "with an invalid enum value" do
131+
let(:b_param_params) { {bike: {frame_material: "1", handlebar_type: "9", frame_model: "Cool model"}} }
132+
it "adds an error rather than raising" do
133+
expect(bike.errors.full_messages).to eq(["Frame material is not valid", "Handlebar type is not valid"])
134+
expect(bike.frame_material).to be_blank
135+
expect(bike.handlebar_type).to be_blank
136+
expect(bike.frame_model).to eq "Cool model"
137+
end
138+
end
139+
130140
context "with organization" do
131141
let(:new_attrs) { {organization:} }
132142

0 commit comments

Comments
 (0)