diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..94e8e06 Binary files /dev/null and b/.DS_Store differ diff --git a/Gemfile b/Gemfile index 3eaff64..24c32b9 100644 --- a/Gemfile +++ b/Gemfile @@ -11,5 +11,6 @@ gem 'rspec-mocks' gem 'solargraph' +gem 'pry' gem 'rake' gem 'rubocop' diff --git a/examples/core_api/main_authenticator.rb b/examples/core_api/main_authenticator.rb index f24e87e..212cdc0 100644 --- a/examples/core_api/main_authenticator.rb +++ b/examples/core_api/main_authenticator.rb @@ -18,15 +18,15 @@ def call cors.methods = %w[GET POST PUT PATCH DELETE OPTIONS] # Define a list of cors headers that are permitted for the request. - cors.headers = %w[X-Custom-Header] + cors.headers = %w[Authorization Content-Type] # or allow all with '*' # Define a the hostname to allow for CORS requests. cors.origin = '*' # or 'example.com' - cors.origin = 'krystal.uk' + + return if request.options? given_token = request.headers['authorization']&.sub(/\ABearer /, '') - case given_token - when 'example' + if given_token == 'example' request.identity = { name: 'Example User', id: 1234 } else raise_error 'CoreAPI/MainAuthenticator/InvalidToken', given_token: given_token.to_s diff --git a/lib/apia/rack.rb b/lib/apia/rack.rb index 485ccfd..761617e 100644 --- a/lib/apia/rack.rb +++ b/lib/apia/rack.rb @@ -79,7 +79,8 @@ def handle_request(env, api_path) validate_api if development? - route = find_route(request_method, api_path) + access_control_request_method = env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']&.upcase if request_method == 'OPTIONS' + route = find_route((access_control_request_method || request_method), api_path) if route.nil? Apia::Notifications.notify(:request_route_not_found, notify_hash) raise RackError.new(404, 'route_not_found', "No route matches '#{api_path}' for #{request_method}") diff --git a/spec/specs/apia/rack_spec.rb b/spec/specs/apia/rack_spec.rb index 8710386..8b6372f 100644 --- a/spec/specs/apia/rack_spec.rb +++ b/spec/specs/apia/rack_spec.rb @@ -154,6 +154,51 @@ def call(_env) expect(Apia::Notifications).to have_received(:notify).with(:request_end, hash_including(path: 'test', method: 'GET', env: kind_of(Hash))).once end + it 'should handle OPTIONS requests' do + controller = Apia::Controller.create('Controller') do + endpoint :test do + action do + response.add_field :hello, 'world' + end + end + end + auth = Apia::Authenticator.create('Authenticator') do + action do + cors.methods = %w[GET OPTIONS] + cors.headers = %w[Authorization Content-Type] + cors.origin = 'example.com' + next if request.options? + + response.add_header 'x-executed', 123 + end + end + api = Apia::API.create('MyAPI') do + authenticator auth + + routes do + get 'test', controller: controller, endpoint: :test + end + end + rack = described_class.new(app, api, 'api/v1') + mock_request = Rack::MockRequest.env_for( + '/api/v1/test', + 'REQUEST_METHOD' => 'OPTIONS', + 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET' + ) + result = rack.call(mock_request) + expect(result).to be_a Array + expect(result[0]).to eq 200 + + headers = result[1] + expect(headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS' + expect(headers['Access-Control-Allow-Headers']).to eq 'Authorization, Content-Type' + expect(headers['Access-Control-Allow-Origin']).to eq 'example.com' + expect(headers['x-executed'].nil?).to be true + + # assert body is empty (does not contain the response from the test endpoint) + expect(result[2][0]).to eq('""') + end + it 'should catch rack errors and return an error triplet' do api = Apia::API.create('MyAPI') rack = described_class.new(app, api, 'api/v1', development: true)