{"id":32094703,"url":"https://github.com/hanami/hanami-controller","last_synced_at":"2026-03-12T09:22:37.703Z","repository":{"id":9141248,"uuid":"10932604","full_name":"hanami/hanami-controller","owner":"hanami","description":"Complete, fast and testable actions for Rack and Hanami","archived":false,"fork":false,"pushed_at":"2026-02-03T09:59:50.000Z","size":1841,"stargazers_count":244,"open_issues_count":8,"forks_count":121,"subscribers_count":23,"default_branch":"main","last_synced_at":"2026-02-03T10:56:47.131Z","etag":null,"topics":["api","controller","hanami","http","rack","ruby"],"latest_commit_sha":null,"homepage":"http://hanamirb.org","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hanami.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"hanami"}},"created_at":"2013-06-25T08:20:55.000Z","updated_at":"2026-02-03T09:59:54.000Z","dependencies_parsed_at":"2023-10-12T06:14:10.305Z","dependency_job_id":"3ffbd4e9-708d-40bd-9d6f-6be26cebe2d3","html_url":"https://github.com/hanami/hanami-controller","commit_stats":{"total_commits":750,"total_committers":86,"mean_commits":8.720930232558139,"dds":0.4706666666666667,"last_synced_commit":"d88f54fdfadf8cbfb44b7dd9d494687ec12644b8"},"previous_names":["hanami/hanami-controller"],"tags_count":69,"template":false,"template_full_name":null,"purl":"pkg:github/hanami/hanami-controller","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanami%2Fhanami-controller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanami%2Fhanami-controller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanami%2Fhanami-controller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanami%2Fhanami-controller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hanami","download_url":"https://codeload.github.com/hanami/hanami-controller/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanami%2Fhanami-controller/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29134200,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-05T20:50:26.975Z","status":"ssl_error","status_checked_at":"2026-02-05T20:49:26.082Z","response_time":65,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","controller","hanami","http","rack","ruby"],"created_at":"2025-10-19T22:01:29.518Z","updated_at":"2026-03-12T09:22:37.695Z","avatar_url":"https://github.com/hanami.png","language":"Ruby","readme":"\u003c!--- This file is synced from hanakai-rb/repo-sync --\u003e\n\n[rubygem]: https://rubygems.org/gems/hanami-controller\n[actions]: https://github.com/hanami/hanami-controller/actions\n\n# Hanami Controller [![Gem Version](https://badge.fury.io/rb/hanami-controller.svg)][rubygem] [![CI Status](https://github.com/hanami/hanami-controller/workflows/CI/badge.svg)][actions]\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"hanami-controller\"\n```\n\nAnd then execute:\n\n```shell\n$ bundle\n```\n\nOr install it yourself as:\n\n```shell\n$ gem install hanami-controller\n```\n\n## Usage\n\nHanami::Controller is a micro library for web frameworks.\nIt works beautifully with [Hanami::Router](https://github.com/hanami/router), but it can be employed everywhere.\nIt's designed to be fast and testable.\n\n### Actions\n\nThe core of this framework are the actions.\nThey are the endpoints that respond to incoming HTTP requests.\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(request, response)\n    response[:article] = ArticleRepository.new.find(request.params[:id])\n  end\nend\n```\n\n`Hanami::Action` follows the Hanami philosophy: a single purpose object with a minimal interface.\n\nIn this case, `Hanami::Action` provides the key public interface of `#call(env)`, making your actions Rack-compatible.\nTo provide custom behaviour when your actions are being called, you can implement `#handle(request, response)`\n\n**An action is an object** and **you have full control over it**.\nIn other words, you have the freedom to instantiate, inject dependencies and test it, both at the unit and integration level.\n\nIn the example below, the default repository is `ArticleRepository`. During a unit test we can inject a stubbed version, and invoke `#call` with the params.\n__We're avoiding HTTP calls__, we're also going to avoid hitting the database (it depends on the stubbed repository), __we're just dealing with message passing__.\nImagine how **fast** the unit test could be.\n\n```ruby\nclass Show \u003c Hanami::Action\n  def initialize(configuration:, repository: ArticleRepository.new)\n    @repository = repository\n    super(configuration: configuration)\n  end\n\n  def handle(request, response)\n    response[:article] = repository.find(request.params[:id])\n  end\n\n  private\n\n  attr_reader :repository\nend\n\nconfiguration = Hanami::Controller::Configuration.new\naction = Show.new(configuration: configuration, repository: ArticleRepository.new)\naction.call(id: 23)\n```\n\n### Params\n\nThe request params are part of the request passed as an argument to the `#handle` method.\nIf routed with *Hanami::Router*, it extracts the relevant bits from the Rack `env` (e.g. the requested `:id`).\nOtherwise everything is passed as is: the full Rack `env` in production, and the given `Hash` for unit tests.\n\nWith `Hanami::Router`:\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(request, *)\n    # ...\n    puts request.params # =\u003e { id: 23 } extracted from Rack env\n  end\nend\n```\n\nStandalone:\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(request, *)\n    # ...\n    puts request.params # =\u003e { :\"rack.version\"=\u003e[1, 2], :\"rack.input\"=\u003e#\u003cStringIO:0x007fa563463948\u003e, ... }\n  end\nend\n```\n\nUnit Testing:\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(request, *)\n    # ...\n    puts request.params # =\u003e { id: 23, key: \"value\" } passed as it is from testing\n  end\nend\n\naction   = Show.new(configuration: configuration)\nresponse = action.call(id: 23, key: \"value\")\n```\n\n#### Allowlisting\n\nParams represent an untrusted input.\nFor security reasons it's recommended to allowlist them.\n\n```ruby\nrequire \"hanami/validations\"\nrequire \"hanami/controller\"\n\nclass Signup \u003c Hanami::Action\n  params do\n    required(:first_name).filled(:str?)\n    required(:last_name).filled(:str?)\n    required(:email).filled(:str?)\n\n    required(:address).schema do\n      required(:line_one).filled(:str?)\n      required(:state).filled(:str?)\n      required(:country).filled(:str?)\n    end\n  end\n\n  def handle(request, *)\n    # Describe inheritance hierarchy\n    puts request.params.class            # =\u003e Signup::Params\n    puts request.params.class.superclass # =\u003e Hanami::Action::Params\n\n    # Allowlist :first_name, but not :admin\n    puts request.params[:first_name]     # =\u003e \"Luca\"\n    puts request.params[:admin]          # =\u003e nil\n\n    # Allowlist nested params [:address][:line_one], not [:address][:line_two]\n    puts request.params[:address][:line_one] # =\u003e \"69 Tender St\"\n    puts request.params[:address][:line_two] # =\u003e nil\n  end\nend\n```\n\n#### Validations \u0026 Coercions\n\nBecause params are a well defined set of data required to fulfill a feature\nin your application, you can validate them. So you can avoid hitting lower MVC layers\nwhen params are invalid.\n\nIf you specify the `:type` option, the param will be coerced.\n\n```ruby\nrequire \"hanami/validations\"\nrequire \"hanami/controller\"\n\nclass Signup \u003c Hanami::Action\n  MEGABYTE = 1024 ** 2\n\n  params do\n    required(:first_name).filled(:str?)\n    required(:last_name).filled(:str?)\n    required(:email).filled?(:str?, format?: /\\A.+@.+\\z/)\n    required(:password).filled(:str?).confirmation\n    required(:terms_of_service).filled(:bool?)\n    required(:age).filled(:int?, included_in?: 18..99)\n    optional(:avatar).filled(size?: 1..(MEGABYTE * 3))\n  end\n\n  def handle(request, *)\n    halt 400 unless request.params.valid?\n    # ...\n  end\nend\n```\n\n### Response\n\nThe output of `#call` is a `Hanami::Action::Response`:\n\n```ruby\nclass Show \u003c Hanami::Action\nend\n\naction = Show.new(configuration: configuration)\naction.call({}) # =\u003e #\u003cHanami::Action::Response:0x00007fe8be968418 @status=200 ...\u003e\n```\n\nThis is the same `response` object passed to `#handle`, where you can use its accessors to explicitly set status, headers, and body:\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(*, response)\n    response.status  = 201\n    response.body    = \"Hi!\"\n    response.headers.merge!(\"X-Custom\" =\u003e \"OK\")\n  end\nend\n\naction = Show.new\naction.call({}) # =\u003e [201, { \"X-Custom\" =\u003e \"OK\" }, [\"Hi!\"]]\n```\n\n### Exposures\n\nIn case you need to send data from the action to other layers of your application, you can use exposures.\nBy default, an action exposes the received params.\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(request, response)\n    response[:article] = ArticleRepository.new.find(request.params[:id])\n  end\nend\n\naction   = Show.new(configuration: configuration)\nresponse = action.call(id: 23)\n\narticle = response[:article]\narticle.class # =\u003e Article\narticle.id # =\u003e 23\n\nresponse.exposures.keys # =\u003e [:params, :article]\n```\n\n### Callbacks\n\nIf you need to execute logic **before** or **after** `#handle` is invoked, you can use _callbacks_.\nThey are useful for shared logic like authentication checks.\n\n```ruby\nclass Show \u003c Hanami::Action\n  before :authenticate, :set_article\n\n  def handle(*)\n  end\n\n  private\n\n  def authenticate\n    # ...\n  end\n\n  # `request` and `response` in the method signature is optional\n  def set_article(request, response)\n    response[:article] = ArticleRepository.new.find(request.params[:id])\n  end\nend\n```\n\nCallbacks can also be expressed as anonymous lambdas:\n\n```ruby\nclass Show \u003c Hanami::Action\n  before { ... } # do some authentication stuff\n  before { |request, response| response[:article] = ArticleRepository.new.find(request.params[:id]) }\n\n  def handle(*)\n  end\nend\n```\n\n### Exceptions management\n\nWhen the app raises an exception, `hanami-controller`, does **NOT** manage it.\nYou can write custom exception handling on per action or configuration basis.\n\nAn exception handler can be a valid HTTP status code (eg. `500`, `401`), or a `Symbol` that represents an action method.\n\n```ruby\nclass Show \u003c Hanami::Action\n  handle_exception StandardError =\u003e 500\n\n  def handle(*)\n    raise\n  end\nend\n\naction = Show.new(configuration: configuration)\naction.call({}) # =\u003e [500, {}, [\"Internal Server Error\"]]\n```\n\nYou can map a specific raised exception to a different HTTP status.\n\n```ruby\nclass Show \u003c Hanami::Action\n  handle_exception RecordNotFound =\u003e 404\n\n  def handle(*)\n    raise RecordNotFound\n  end\nend\n\naction = Show.new(configuration: configuration)\naction.call({}) # =\u003e [404, {}, [\"Not Found\"]]\n```\n\nYou can also define custom handlers for exceptions.\n\n```ruby\nclass Create \u003c Hanami::Action\n  handle_exception ArgumentError =\u003e :my_custom_handler\n\n  def handle(*)\n    raise ArgumentError.new(\"Invalid arguments\")\n  end\n\n  private\n\n  def my_custom_handler(request, response, exception)\n    response.status = 400\n    response.body   = exception.message\n  end\nend\n\naction = Create.new(configuration: configuration)\naction.call({}) # =\u003e [400, {}, [\"Invalid arguments\"]]\n```\n\nException policies can be defined globally via configuration:\n\n```ruby\nconfiguration = Hanami::Controller::Configuration.new do |config|\n  config.handle_exception RecordNotFound =\u003e 404\nend\n\nclass Show \u003c Hanami::Action\n  def handle(*)\n    raise RecordNotFound\n  end\nend\n\naction = Show.new(configuration: configuration)\naction.call({}) # =\u003e [404, {}, [\"Not Found\"]]\n```\n\n#### Inherited Exceptions\n\n```ruby\nclass MyCustomException \u003c StandardError\nend\n\nmodule Articles\n  class Index \u003c Hanami::Action\n    handle_exception MyCustomException =\u003e :handle_my_exception\n\n    def handle(*)\n      raise MyCustomException\n    end\n\n    private\n\n    def handle_my_exception(request, response, exception)\n      # ...\n    end\n  end\n\n  class Show \u003c Hanami::Action\n    handle_exception StandardError =\u003e :handle_standard_error\n\n    def handle(*)\n      raise MyCustomException\n    end\n\n    private\n\n    def handle_standard_error(request, response, exception)\n      # ...\n    end\n  end\nend\n\nArticles::Index.new.call({}) # =\u003e `handle_my_exception` will be invoked\nArticles::Show.new.call({})  # =\u003e `handle_standard_error` will be invoked,\n                             #   because `MyCustomException` inherits from `StandardError`\n```\n\n### Throwable HTTP statuses\n\nWhen `#halt` is used with a valid HTTP code, it stops the execution and sets the proper status and body for the response:\n\n```ruby\nclass Show \u003c Hanami::Action\n  before :authenticate!\n\n  def handle(*)\n    # ...\n  end\n\n  private\n\n  def authenticate!\n    halt 401 unless authenticated?\n  end\nend\n\naction = Show.new(configuration: configuration)\naction.call({}) # =\u003e [401, {}, [\"Unauthorized\"]]\n```\n\nAlternatively, you can specify a custom message.\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(request, response)\n    response[:droid] = DroidRepository.new.find(request.params[:id]) or not_found\n  end\n\n  private\n\n  def not_found\n    halt 404, \"This is not the droid you're looking for\"\n  end\nend\n\naction = Show.new(configuration: configuration)\naction.call({}) # =\u003e [404, {}, [\"This is not the droid you're looking for\"]]\n```\n\n### Cookies\n\nYou can read the original cookies sent from the HTTP client via `request.cookies`.\nIf you want to send cookies in the response, use `response.cookies`.\n\nThey are read as a Hash from Rack env:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/cookies\"\n\nclass ReadCookiesFromRackEnv \u003c Hanami::Action\n  include Hanami::Action::Cookies\n\n  def handle(request, *)\n    # ...\n    request.cookies[:foo] # =\u003e \"bar\"\n  end\nend\n\naction = ReadCookiesFromRackEnv.new(configuration: configuration)\naction.call({\"HTTP_COOKIE\" =\u003e \"foo=bar\"})\n```\n\nThey are set like a Hash:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/cookies\"\n\nclass SetCookies \u003c Hanami::Action\n  include Hanami::Action::Cookies\n\n  def handle(*, response)\n    # ...\n    response.cookies[:foo] = \"bar\"\n  end\nend\n\naction = SetCookies.new(configuration: configuration)\naction.call({}) # =\u003e [200, {\"Set-Cookie\" =\u003e \"foo=bar\"}, \"...\"]\n```\n\nThey are removed by setting their value to `nil`:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/cookies\"\n\nclass RemoveCookies \u003c Hanami::Action\n  include Hanami::Action::Cookies\n\n  def handle(*, response)\n    # ...\n    response.cookies[:foo] = nil\n  end\nend\n\naction = RemoveCookies.new(configuration: configuration)\naction.call({}) # =\u003e [200, {\"Set-Cookie\" =\u003e \"foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000\"}, \"...\"]\n```\n\nDefault values can be set in configuration, but overridden case by case.\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/cookies\"\n\nconfiguration = Hanami::Controller::Configuration.new do |config|\n  config.cookies(max_age: 300) # 5 minutes\nend\n\nclass SetCookies \u003c Hanami::Action\n  include Hanami::Action::Cookies\n\n  def handle(*, response)\n    # ...\n    response.cookies[:foo] = { value: \"bar\", max_age: 100 }\n  end\nend\n\naction = SetCookies.new(configuration: configuration)\naction.call({}) # =\u003e [200, {\"Set-Cookie\" =\u003e \"foo=bar; max-age=100;\"}, \"...\"]\n```\n\n### Sessions\n\nActions have builtin support for Rack sessions.\nSimilarly to cookies, you can read the session sent by the HTTP client via\n`request.session`, and also manipulate it via `response.session`.\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/session\"\n\nclass ReadSessionFromRackEnv \u003c Hanami::Action\n  include Hanami::Action::Session\n\n  def handle(request, *)\n    # ...\n    request.session[:age] # =\u003e \"35\"\n  end\nend\n\naction = ReadSessionFromRackEnv.new(configuration: configuration)\naction.call({ \"rack.session\" =\u003e { \"age\" =\u003e \"35\" } })\n```\n\nValues can be set like a Hash:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/session\"\n\nclass SetSession \u003c Hanami::Action\n  include Hanami::Action::Session\n\n  def handle(*, response)\n    # ...\n    response.session[:age] = 31\n  end\nend\n\naction = SetSession.new(configuration: configuration)\naction.call({}) # =\u003e [200, {\"Set-Cookie\"=\u003e\"rack.session=...\"}, \"...\"]\n```\n\nValues can be removed like a Hash:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/session\"\n\nclass RemoveSession \u003c Hanami::Action\n  include Hanami::Action::Session\n\n  def handle(*, response)\n    # ...\n    response.session[:age] = nil\n  end\nend\n\naction = RemoveSession.new(configuration: configuration)\naction.call({}) # =\u003e [200, {\"Set-Cookie\"=\u003e\"rack.session=...\"}, \"...\"] it removes that value from the session\n```\n\nWhile Hanami::Controller supports sessions natively, it's **session store agnostic**.\nYou have to specify the session store in your Rack middleware configuration (eg `config.ru`).\n\n```ruby\nuse Rack::Session::Cookie, secret: SecureRandom.hex(64)\nrun Show.new(configuration: configuration)\n```\n\n### HTTP Cache\n\nHanami::Controller sets your headers correctly according to RFC 2616 / 14.9 for more on standard cache control directives: http://tools.ietf.org/html/rfc2616#section-14.9.1\n\nYou can easily set the Cache-Control header for your actions:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/cache\"\n\nclass HttpCacheController \u003c Hanami::Action\n  include Hanami::Action::Cache\n  cache_control :public, max_age: 600 # =\u003e Cache-Control: public, max-age=600\n\n  def handle(*)\n    # ...\n  end\nend\n```\n\nExpires header can be specified using `expires` method:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/cache\"\n\nclass HttpCacheController \u003c Hanami::Action\n  include Hanami::Action::Cache\n  expires 60, :public, max_age: 600 # =\u003e Expires: Sun, 03 Aug 2014 17:47:02 GMT, Cache-Control: public, max-age=600\n\n  def handle(*)\n    # ...\n  end\nend\n```\n\n### Conditional Get\n\nAccording to HTTP specification, conditional GETs provide a way for web servers to inform clients that the response to a GET request hasn't change since the last request returning a `304 (Not Modified)` response.\n\nPassing the `HTTP_IF_NONE_MATCH` (content identifier) or `HTTP_IF_MODIFIED_SINCE` (timestamp) headers allows the web server define if the client has a fresh version of a given resource.\n\nYou can easily take advantage of Conditional Get using `#fresh` method:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/cache\"\n\nclass ConditionalGetController \u003c Hanami::Action\n  include Hanami::Action::Cache\n\n  def handle(*)\n    # ...\n    fresh etag: resource.cache_key\n    # =\u003e halt 304 with header IfNoneMatch = resource.cache_key\n  end\nend\n```\n\nIf `resource.cache_key` is equal to `IfNoneMatch` header, then hanami will `halt 304`.\n\nAn alternative to hashing based check, is the time based check:\n\n```ruby\nrequire \"hanami/controller\"\nrequire \"hanami/action/cache\"\n\nclass ConditionalGetController \u003c Hanami::Action\n  include Hanami::Action::Cache\n\n  def handle(*)\n    # ...\n    fresh last_modified: resource.updated_at\n    # =\u003e halt 304 with header IfModifiedSince = resource.updated_at.httpdate\n  end\nend\n```\n\nIf `resource.updated_at` is equal to `IfModifiedSince` header, then hanami will `halt 304`.\n\n### Redirect\n\nIf you need to redirect the client to another resource, use `response.redirect_to`:\n\n```ruby\nclass Create \u003c Hanami::Action\n  def handle(*, response)\n    # ...\n    response.redirect_to \"http://example.com/articles/23\"\n  end\nend\n\naction = Create.new(configuration: configuration)\naction.call({ article: { title: \"Hello\" }}) # =\u003e [302, {\"Location\" =\u003e \"/articles/23\"}, \"\"]\n```\n\nYou can also redirect with a custom status code:\n\n```ruby\nclass Create \u003c Hanami::Action\n  def handle(*, response)\n    # ...\n    response.redirect_to \"http://example.com/articles/23\", status: 301\n  end\nend\n\naction = Create.new(configuration: configuration)\naction.call({ article: { title: \"Hello\" }}) # =\u003e [301, {\"Location\" =\u003e \"/articles/23\"}, \"\"]\n```\n\n### MIME Types\n\n`Hanami::Action` automatically sets the `Content-Type` header, according to the request.\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(*)\n  end\nend\n\naction = Show.new(configuration: configuration)\n\nresponse = action.call({ \"HTTP_ACCEPT\" =\u003e \"*/*\" }) # Content-Type \"application/octet-stream\"\nresponse.format                                    # :all\n\nresponse = action.call({ \"HTTP_ACCEPT\" =\u003e \"text/html\" }) # Content-Type \"text/html\"\nresponse.format                                          # :html\n```\n\nHowever, you can force this value:\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(*, response)\n    # ...\n    response.format = :json\n  end\nend\n\naction = Show.new(configuration: configuration)\n\nresponse = action.call({ \"HTTP_ACCEPT\" =\u003e \"*/*\" }) # Content-Type \"application/json\"\nresponse.format                                    # :json\n\nresponse = action.call({ \"HTTP_ACCEPT\" =\u003e \"text/html\" }) # Content-Type \"application/json\"\nresponse.format                                          # :json\n```\n\nYou can restrict the accepted MIME types:\n\n```ruby\nclass Show \u003c Hanami::Action\n  accept :html, :json\n\n  def handle(*)\n    # ...\n  end\nend\n\n# When called with \"*/*\"              =\u003e 200\n# When called with \"text/html\"        =\u003e 200\n# When called with \"application/json\" =\u003e 200\n# When called with \"application/xml\"  =\u003e 415\n```\n\nYou can check if the requested MIME type is accepted by the client.\n\n```ruby\nclass Show \u003c Hanami::Action\n  def handle(request, response)\n    # ...\n    # @_env[\"HTTP_ACCEPT\"] # =\u003e \"text/html,application/xhtml+xml,application/xml;q=0.9\"\n\n    request.accept?(\"text/html\")        # =\u003e true\n    request.accept?(\"application/xml\")  # =\u003e true\n    request.accept?(\"application/json\") # =\u003e false\n    response.format                     # :html\n\n\n    # @_env[\"HTTP_ACCEPT\"] # =\u003e \"*/*\"\n\n    request.accept?(\"text/html\")        # =\u003e true\n    request.accept?(\"application/xml\")  # =\u003e true\n    request.accept?(\"application/json\") # =\u003e true\n    response.format                     # :html\n  end\nend\n```\n\nHanami::Controller is shipped with an extensive list of the most common MIME types.\nAlso, you can register your own:\n\n```ruby\nconfiguration = Hanami::Controller::Configuration.new do |config|\n  config.format custom: \"application/custom\"\nend\n\nclass Index \u003c Hanami::Action\n  def handle(*)\n  end\nend\n\naction = Index.new(configuration: configuration)\n\nresponse = action.call({ \"HTTP_ACCEPT\" =\u003e \"application/custom\" }) # =\u003e Content-Type \"application/custom\"\nresponse.format                                                   # =\u003e :custom\n\nclass Show \u003c Hanami::Action\n  def handle(*, response)\n    # ...\n    response.format = :custom\n  end\nend\n\naction = Show.new(configuration: configuration)\n\nresponse = action.call({ \"HTTP_ACCEPT\" =\u003e \"*/*\" }) # =\u003e Content-Type \"application/custom\"\nresponse.format                                    # =\u003e :custom\n```\n\n### Streamed Responses\n\nWhen the work to be done by the server takes time, it may be a good idea to stream your response. Here's an example of a streamed CSV.\n\n```ruby\nconfiguration = Hanami::Controller::Configuration.new do |config|\n  config.format csv: 'text/csv'\nend\n\nclass Csv \u003c Hanami::Action\n  def handle(*, response)\n    response.format = :csv\n    response.body = Enumerator.new do |yielder|\n      yielder \u003c\u003c csv_header\n\n      # Expensive operation is streamed as each line becomes available\n      csv_body.each_line do |line|\n        yielder \u003c\u003c line\n      end\n    end\n  end\nend\n```\n\nNote:\n* In development, Hanami' code reloading needs to be disabled for streaming to work. This is because `Shotgun` interferes with the streaming action. You can disable it like this `hanami server --code-reloading=false`\n* Streaming does not work with WEBrick as it buffers its response. We recommend using `puma`, though you may find success with other servers\n\n### No rendering, please\n\nHanami::Controller is designed to be a pure HTTP endpoint, rendering belongs to other layers of MVC.\nYou can set the body directly (see [response](#response)), or use [Hanami::View](https://github.com/hanami/view).\n\n### Controllers\n\nA Controller is nothing more than a logical group of actions: just a Ruby module.\n\n```ruby\nmodule Articles\n  class Index \u003c Hanami::Action\n    # ...\n  end\n\n  class Show \u003c Hanami::Action\n    # ...\n  end\nend\n\nArticles::Index.new(configuration: configuration).call({})\n```\n\n### Hanami::Router integration\n\n```ruby\nrequire \"hanami/router\"\nrequire \"hanami/controller\"\n\nmodule Web\n  module Controllers\n    module Books\n      class Show \u003c Hanami::Action\n        def handle(*)\n        end\n      end\n    end\n  end\nend\n\nconfiguration = Hanami::Controller::Configuration.new\nrouter = Hanami::Router.new(configuration: configuration, namespace: Web::Controllers) do\n  get \"/books/:id\", \"books#show\"\nend\n```\n\n### Rack integration\n\nHanami::Controller is compatible with Rack. If you need to use any Rack middleware, please mount them in `config.ru`.\n\n### Configuration\n\nHanami::Controller can be configured via `Hanami::Controller::Configuration`.\nIt supports a few options:\n\n```ruby\nrequire \"hanami/controller\"\n\nconfiguration = Hanami::Controller::Configuration.new do |config|\n  # If the given exception is raised, return that HTTP status\n  # It can be used multiple times\n  # Argument: hash, empty by default\n  #\n  config.handle_exception ArgumentError =\u003e 404\n\n  # Register a format to MIME type mapping\n  # Argument: hash, key: format symbol, value: MIME type string, empty by default\n  #\n  config.format custom: \"application/custom\"\n\n  # Define a default format to set as `Content-Type` header for response,\n  # unless otherwise specified.\n  # If not defined here, it will return Rack's default: `application/octet-stream`\n  # Argument: symbol, it should be already known. defaults to `nil`\n  #\n  config.default_response_format = :html\n\n  # Define a default charset to return in the `Content-Type` response header\n  # If not defined here, it returns `utf-8`\n  # Argument: string, defaults to `nil`\n  #\n  config.default_charset = \"koi8-r\"\nend\n```\n\n### Thread safety\n\nAn Action is **immutable**, it works without global state, so it's thread-safe by design.\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n\n## Links\n\n- [User documentation](https://hanamirb.org)\n- [API documentation](http://rubydoc.info/gems/hanami-controller)\n\n\n## License\n\nSee `LICENSE` file.\n\n","funding_links":["https://github.com/sponsors/hanami"],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhanami%2Fhanami-controller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhanami%2Fhanami-controller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhanami%2Fhanami-controller/lists"}