Skip to content

Respond with 404 Not Found if project not found in Api::SchoolProjectsController actions #564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

floehopper
Copy link
Contributor

Previously when a request was made to either of the actions in Api::SchoolProjectsController for a project that does not exist, the following exception was raised, because the call to Project.find_by returned nil:

NoMethodError: undefined method `school_project' for nil:NilClass

Unhandled exceptions are rescued by the default ActionDispatch::ShowExceptions middleware. In turn this uses the default exceptions app, ActionDispatch::PublicExceptions to render a static HTML error page from public/500.html in this case.

Since this is an API endpoint, ideally it should always respond with JSON in the response body, otherwise it makes writing client code a lot harder. Indeed my actual motivation for fixing this is, because we've been seeing JSON:ParseError exceptions raised in experience-cs.

I've fixed the problem by using Project.find_by! instead of Project.find_by. This means that if the project is not found, a ActiveRecord::RecordNotFound exception is raised. This in turn is rescued by the rescue_from block in ApiController and the action responds with a 404 Not Found head response, i.e. with no body.

This head response seems to be handled OK by the HTTParty code in experience-cs and thus it fixes the problem I was trying to address.

The equivalent client-side code in editor-standalone was already throwing an AxiosError exception due to the 500 response code (not being a 2XX code) and it will continue to throw the same exception, albeit with a payload reflecting the response code being 404 instead of 500 and the body being empty instead of being the Rails 500 HTML error page.

There is no specific error-handling code in editor-standalone catching this exception, so I assume it's just relying on the generic error handling for the React app. Thus I think the user-facing behaviour should be unchanged.

Addresses https://github.com/RaspberryPiFoundation/experience-cs/issues/828.

Postscript

While investigating this issue, I've uncovered a number of other problems with the Api::SchoolProjectsController code and its specs, but I've intentionally not addressed them here in order to keep the scope of this PR as small as possible and keep it focussed on fixing the problem at hand. However, in case it's useful, I'll list the issues here:

  • The call to load_and_authorize_resource :project is not working as intended:
    • it's not even attempting to load the Project instance (even if it was, it would be attempting to call Project.find(params[:id) and not Project.find_by!(identifier: params[:id])
    • it's not authorizing the user can take the action against an instance of a Project (it seems to be authorizing the user can show the Project class, but it seems unlikely this is what was intended)
    • I can't tell from the git history why this call to load_and_authorize_resource was added and no tests fail if I remove it!
    • This issue is closely related to Fix authorization in Api::ProjectsController #553.
  • Given that Api::ProjectsController uses a custom ProjectLoader class which takes locale into account, I would've thought that this Api::SchoolProjectsController should also use the same lookup mechanism, but it doesn't.
  • There are no examples testing the presence of the before_action :authorize_user call.
  • The call to ProfileApiMock#stub_profile_api_list_school_students in spec/requests/school_projects/show_finished_spec.rb & spec/requests/school_projects/set_finished_spec.rb seem unnecessary and are potentially confusing. This helper method stubs ProfileApiClient.list_school_students, but this method is never called by code exercised by those specs. This seems to be a wider problem where calls to ProfileApiMock#stub_profile_api_list_school_students have been added to specs that don't need it. Perhaps they never needed it or the need for them has gone away.
  • This isn't specific to this controller, but I think it's not great that any unhandled exceptions in API controllers result in an HTML response. Exceptions that occur in the "operation" classes in lib/concepts/*/operations/ are usually rescued and the action responds with a JSON body. However, for exceptions that occur outside those operations (e.g. in the actions themselves) just result in the 500 error with HTML body.

Previously when a request was made to either of the actions in
`Api::SchoolProjectsController` for a project that does not exist, the
following exception was raised:

    NoMethodError: undefined method `school_project' for nil:NilClass

Unhandled exceptions are rescued by the default
`ActionDispatch::ShowExceptions` middleware [1]. In turn this uses the
default exceptions app, `ActionDispatch::PublicExceptions` [2] to render
a static HTML error page from `public/500.html` in this case.

Since this is an API endpoint, ideally it should always respond with
JSON in the response body, otherwise it makes writing client code a lot
harder. Indeed my actual motivation for fixing this is, because we've
been seeing JSON:ParseError exceptions raised in experience-cs [3].

I've fixed the problem by using `Project.find_by!` instead of
`Project.find_by`. This means that if the project is not found, a
`ActiveRecord::RecordNotFound` exception is raised. This in turn is
rescued by the `rescue_from` block in `ApiController` [4] and the action
responds with a 404 Not Found head response, i.e. with no body.

This head response seems to be handled OK by the HTTParty code in
experience-cs [5] and thus it fixes the problem I was trying to address.

The equivalent client-side code in editor-standalone [6] was already
throwing an AxisError exception due to the 500 response code (not being
a 2XX code [7]) and it will continue to throw the same exception, albeit
with a payload reflecting the response code being 404 instead of 500 and
the body being empty instead of being the Rails 500 HTML error page.

There is no specific error-handling code in editor-standalone catching
this exception, so I assume it's just relying on the generic error
handling for the React app. Thus I think the user-facing behaviour
should be unchanged.

[1]: https://api.rubyonrails.org/v7.1.3.4/classes/ActionDispatch/ShowExceptions.html
[2]: https://api.rubyonrails.org/v7.1.3.4/classes/ActionDispatch/PublicExceptions.html
[3]: https://raspberrypifoundation.sentry.io/issues/6708738500/
[4]: https://github.com/RaspberryPiFoundation/editor-api/blob/c37ab30714edeb08beaa1929970772545f38e93d/app/controllers/api_controller.rb#L9
[5]: https://github.com/RaspberryPiFoundation/experience-cs/blob/7b559bef916e0a32b3174f2391dcc587a8c4fe3b/lib/editor_api/client.rb
[6]: https://github.com/RaspberryPiFoundation/editor-standalone/blob/f1286121460e79da7ffa7431487a2fb0872f2ae5/src/utils/apiCallHandler/projects.js#L71-L91
[7]: https://axios-http.com/docs/handling_errors
@cla-bot cla-bot bot added the cla-signed label Jul 2, 2025
@raspberrypiherokubot raspberrypiherokubot temporarily deployed to editor-api-p-fix-api-sc-irvl8v July 2, 2025 07:51 Inactive
Copy link
Contributor

@sebjacobs sebjacobs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ Thanks @floehopper. It's great that you've been able to wrap some simple specs around this bug. I'm also really impressed how you've managed to avoid scope creep here.

@floehopper
Copy link
Contributor Author

Adrian has given me the go-ahead to merge this.

@floehopper floehopper merged commit 7cf9faf into main Jul 2, 2025
3 checks passed
@floehopper floehopper deleted the fix-api-school-projects-controller-show-error-responses branch July 2, 2025 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants