How do you set up a file upload route correctly for a JSON-REST API?

Hey :waving_hand:

I have a slice with a JSON Rest API. So the main actions format is :json. In this api i also need an endpoint to upload files. Because of this I cannot set config.actions.format :json at the slice config. If I do it, I get an Unsupported Media Type response.

module Documents
  class Slice < Hanami::Slice
    # config.actions.format :json 
    config.db.import_from_parent = true
  end
end
module MyDocumentsApp
  class Routes < Hanami::Routes
    # Add your routes here. See https://guides.hanamirb.org/routing/overview/ for details.

    slice :documents, at: "/documents" do
      scope "upload" do
        use :body_parser, :form
        post ":folder", to: "upload"
      end
      scope "api" do
        use :body_parser, :json
        post "users", to: "api.users.create"
      end
    end
  end
end

I tried to set explicitly set the action format at the Upload-Action

module Documents
  module Actions
    class Upload < Documents::Action
      config.formats.add :multipart, "multipart/form-data"
      config.format :multipart
      
      def handle(req, res)
        # ...
      end
    end
  end
end

but still get the Unsupported Media Type response. The only solution at the moment is that I do not set the format at slice-config level. In the upload action, I am not allowed to set a custom format.

Do you know a better solution?

thanks

Thanks for bringing this question here, @wuarmin.

Looking at what you’ve shared, I can make a simple reproduction where the config, at least, looks correct:

# config/app.rb

module FormatsApp
  class App < Hanami::App
    config.actions.format :json
  end
end
# app/actions/json_action.rb

module FormatsApp
  module Actions
    class JSONAction < FormatsApp::Action
    end
  end
end
# app/actions/upload_action.rb

module FormatsApp
  module Actions
    class UploadAction < FormatsApp::Action
      config.formats.add :multipart, "multipart/form-data"
      config.format :multipart
    end
  end
end

Then in the console, if I inspect the (private) config for each of these actions, their formats look correct:

# JSON action uses format values from the app (i.e. slice)
formats_app[development]> app["actions.json_action"].send(:config).formats
=> #<Hanami::Action::Config::Formats values=[:json] mapping={"application/octet-stream" => :all, "*/*" => :all}>

# Upload action reflects its own format config
formats_app[development]> app["actions.upload_action"].send(:config).formats
=> #<Hanami::Action::Config::Formats values=[:multipart] mapping={"application/octet-stream" => :all, "*/*" => :all, "multipart/form-data" => :multipart}>

If you’re getting an “Unsupported Media Type” error, then this would be coming from Hanami Action’s internal enforce_accepted_mime_types method.

This method in turn calls our mime module’s enforce_content_type method, which is hopefully fairly straightforward to read:

def enforce_content_type(request, config)
  content_type = request.content_type

  return if content_type.nil?

  return if accepted_mime_type?(content_type, config)

  yield
end

What I might suggest you do is bundle open Hanami-controller and drop a binding.irb into this method above and poke around. If you could share what config, content_type, and accepted_mime_type?(content_type, config) look like, then hopefully I can help you narrow in on what’s going on.

Thank you for helping figure this out!

Thanks @timriley!
The output for the upload-endpoint is:

>>> content_type: "multipart/form-data; boundary=------------------------l66OQWe0cxN8bD2hp6nkC9"
>>> config.formats.values: [:multipart]
>>> config.formats.mapping: {"application/octet-stream" => :all, "*/*" => :all, "multipart/form-data" => :multipart}
>>> accepted_mime_types(config): ["multipart/form-data"]

So the issue lies here:

Here’s the debug output:

>>> v1: "multipart" v2: "form-data; boundary=------------------------NYJ4v948lVEtMlrLscIhv8"
>>> m1: "multipart" m2: "form-data"

The m2-v2-match fails.

@timriley: What do you think? I think we should fix this at Rack? I opened an issue there:

I opened a PR to fix the issue:

Just for info:
I have also tested Accept-header. Everything works correctly here.

1 Like