Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
540a299
Improved REST endpoint exception handling, see #15335
jbertouch Apr 5, 2016
0906aa1
Tidied up formatting and comments
jbertouch Apr 21, 2016
1d3dcdb
Tests for #15335
jbertouch Apr 21, 2016
550cc94
Cleaned up comments, added section number
jbertouch Apr 21, 2016
321af02
Swapped out tab indents for space indents
jbertouch Apr 21, 2016
f2af227
Merge remote-tracking branch 'elastic/master' into issue_15335
jbertouch Apr 21, 2016
20a2273
Test class now extends ESSingleNodeTestCase
jbertouch Apr 22, 2016
447a182
Capture RestResponse so it can be examined in test cases
jbertouch Apr 29, 2016
7d1cc14
Refactored class name, included feedback
jbertouch Apr 29, 2016
f661a2f
Unit test for REST error handling enhancements
jbertouch Apr 29, 2016
fc5bbeb
Cleaned up formatting
jbertouch Apr 29, 2016
b38aed8
New constructor to set HTTP method
jbertouch May 4, 2016
b08ee7f
Refactored FakeRestRequest, streamlined test case.
jbertouch May 4, 2016
e85f537
Merge remote-tracking branch 'elastic/master' into issue_15335
jbertouch Aug 1, 2016
208ba18
Cleaned up conflicts
jbertouch Aug 1, 2016
a753560
Tests for #15335
jbertouch Aug 3, 2016
b0cc920
Added functionality to ignore or include path wildcards
jbertouch Aug 3, 2016
cc17f3a
Further enhancements to request handling
jbertouch Aug 3, 2016
622093c
Cosmetic fixes
jbertouch Oct 30, 2016
daec2c4
Refactored method handlers
jbertouch Oct 31, 2016
6901745
Merge remote-tracking branch 'elastic/master' into issue_15335
jbertouch Oct 31, 2016
8252921
Merge remote-tracking branch 'elastic/master' into issue_15335
jbertouch Nov 1, 2016
e6d2777
Removed redundant import
jbertouch Nov 1, 2016
4d67a91
Updated integration tests
jbertouch Nov 3, 2016
474d001
Refactoring to address issue #17853
jbertouch Nov 9, 2016
55680ee
Cleaned up test assertions
jbertouch Nov 9, 2016
c10967d
Fixed edge case if OPTIONS method randomly selected as invalid method
jbertouch Nov 9, 2016
99a80a9
Merge remote-tracking branch 'origin/master' into pr/17916
dakrone Apr 12, 2017
d56b72b
Remove redundant static modifier
dakrone Apr 12, 2017
42aeeb3
Hook the multiple PathTrie attempts into RestHandler.dispatchRequest
dakrone Apr 12, 2017
582b2ce
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone Apr 25, 2017
940c39f
Add missing space
dakrone Apr 25, 2017
6a0840c
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone May 2, 2017
dd02ffb
Correctly retrieve new handler for each Trie strategy
dakrone May 8, 2017
c98d5ec
Only copy headers to threadcontext once
dakrone May 8, 2017
23d05ba
Fix test after REST header copying moved higher up
dakrone May 8, 2017
715ade6
Restore original params when trying the next trie candidate
dakrone May 8, 2017
bc42fa2
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone May 16, 2017
b05f842
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone Jun 2, 2017
65f4fd2
Remove OPTIONS for invalidHttpMethodArray so a 405 is guaranteed in t…
dakrone Jun 2, 2017
007445a
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone Jun 2, 2017
b3627df
Re-add the fix I already added and got removed during merge :-/
dakrone Jun 2, 2017
ab06ef2
Add missing GET method to test
dakrone Jun 5, 2017
c561298
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone Jun 5, 2017
56ca00c
Add documentation to migration guide about breaking 404 -> 405 changes
dakrone Jun 5, 2017
9e91874
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone Jun 8, 2017
29adb89
Explain boolean response, pull into local var
dakrone Jun 14, 2017
8c00ce4
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone Jun 14, 2017
95b07d2
fixup! Explain boolean response, pull into local var
dakrone Jun 14, 2017
a36a04c
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone Jun 21, 2017
14cc803
Encapsulate multiple HTTP methods into PathTrie<MethodHandlers>
dakrone Jun 21, 2017
24d818a
Add PathTrie.retrieveAll where all matching modes can be retrieved
dakrone Jun 22, 2017
c168066
Include body of error with 405 responses to give hint about valid met…
dakrone Jun 22, 2017
222bdde
Fix missing usageService handler addition
dakrone Jun 22, 2017
6cc244e
Initialize PathTrieIterator modes with Arrays.asList
dakrone Jun 28, 2017
da41feb
Merge remote-tracking branch 'origin/master' into improve-rest-error-…
dakrone Jul 5, 2017
9524f14
Use "== false" instead of !
dakrone Jul 7, 2017
e87321f
Missing paren :-/
dakrone Jul 7, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 65 additions & 15 deletions core/src/main/java/org/elasticsearch/rest/RestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@
import java.util.function.UnaryOperator;

import static org.elasticsearch.rest.RestStatus.BAD_REQUEST;
import static org.elasticsearch.rest.RestStatus.METHOD_NOT_ALLOWED;
import static org.elasticsearch.rest.RestStatus.FORBIDDEN;
import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR;
import static org.elasticsearch.rest.RestStatus.NOT_ACCEPTABLE;
import static org.elasticsearch.rest.RestStatus.OK;
import static org.elasticsearch.rest.BytesRestResponse.TEXT_CONTENT_TYPE;

public class RestController extends AbstractComponent implements HttpServerTransport.Dispatcher {

Expand Down Expand Up @@ -141,11 +143,11 @@ public void registerWithDeprecatedHandler(RestRequest.Method method, String path
}

/**
* Registers a REST handler to be executed when the provided method and path match the request.
* Registers a REST handler to be executed when one of the provided methods and path match the request.
*
* @param method GET, POST, etc.
* @param path Path to handle (e.g., "/{index}/{type}/_bulk")
* @param handler The handler to actually execute
* @param method GET, POST, etc.
*/
public void registerHandler(RestRequest.Method method, String path, RestHandler handler) {
if (handler instanceof BaseRestHandler) {
Expand Down Expand Up @@ -183,11 +185,8 @@ public void dispatchRequest(RestRequest request, RestChannel channel, ThreadCont
}

@Override
public void dispatchBadRequest(
final RestRequest request,
final RestChannel channel,
final ThreadContext threadContext,
final Throwable cause) {
public void dispatchBadRequest(final RestRequest request, final RestChannel channel,
final ThreadContext threadContext, final Throwable cause) {
try {
final Exception e;
if (cause == null) {
Expand All @@ -211,7 +210,7 @@ public void dispatchBadRequest(
* Dispatch the request, if possible, returning true if a response was sent or false otherwise.
*/
boolean dispatchRequest(final RestRequest request, final RestChannel channel, final NodeClient client,
ThreadContext threadContext, final Optional<RestHandler> mHandler) throws Exception {
final Optional<RestHandler> mHandler) throws Exception {
final int contentLength = request.hasContent() ? request.content().length() : 0;

RestChannel responseChannel = channel;
Expand All @@ -228,6 +227,7 @@ boolean dispatchRequest(final RestRequest request, final RestChannel channel, fi
"] does not support stream parsing. Use JSON or SMILE instead"));
requestHandled = true;
} else if (mHandler.isPresent()) {

try {
if (canTripCircuitBreaker(mHandler)) {
inFlightRequestsBreaker(circuitBreakerService).addEstimateBytesAndMaybeBreak(contentLength, "<http_request>");
Expand All @@ -246,10 +246,19 @@ boolean dispatchRequest(final RestRequest request, final RestChannel channel, fi
requestHandled = true;
}
} else {
if (request.method() == RestRequest.Method.OPTIONS) {
// when we have OPTIONS request, simply send OK by default (with the Access Control Origin header which gets automatically added)

channel.sendResponse(new BytesRestResponse(OK, BytesRestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY));
// Get the map of matching handlers for a request, for the full set of HTTP methods.
final Set<RestRequest.Method> validMethodSet = getValidHandlerMethodSet(request);
if (validMethodSet.size() > 0
&& validMethodSet.contains(request.method()) == false
&& request.method() != RestRequest.Method.OPTIONS) {
// If an alternative handler for an explicit path is registered to a
// different HTTP method than the one supplied - return a 405 Method
// Not Allowed error.
handleUnsupportedHttpMethod(request, channel, validMethodSet);
requestHandled = true;
} else if (validMethodSet.contains(request.method()) == false
&& (request.method() == RestRequest.Method.OPTIONS)) {
handleOptionsRequest(request, channel, validMethodSet);
requestHandled = true;
} else {
requestHandled = false;
Expand All @@ -263,9 +272,9 @@ boolean dispatchRequest(final RestRequest request, final RestChannel channel, fi
* If a request contains content, this method will return {@code true} if the {@code Content-Type} header is present, matches an
* {@link XContentType} or the handler supports a content stream and the content type header is for newline delimited JSON,
*/
private boolean hasContentType(final RestRequest restRequest, final RestHandler restHandler) {
private static boolean hasContentType(final RestRequest restRequest, final RestHandler restHandler) {
if (restRequest.getXContentType() == null) {
if (restHandler != null && restHandler.supportsContentStream() && restRequest.header("Content-Type") != null) {
if (restHandler.supportsContentStream() && restRequest.header("Content-Type") != null) {
final String lowercaseMediaType = restRequest.header("Content-Type").toLowerCase(Locale.ROOT);
// we also support newline delimited JSON: http://specs.okfnlabs.org/ndjson/
if (lowercaseMediaType.equals("application/x-ndjson")) {
Expand Down Expand Up @@ -325,7 +334,7 @@ void tryAllHandlers(final RestRequest request, final RestChannel channel, final
Iterator<MethodHandlers> allHandlers = getAllHandlers(request);
for (Iterator<MethodHandlers> it = allHandlers; it.hasNext(); ) {
final Optional<RestHandler> mHandler = Optional.ofNullable(it.next()).flatMap(mh -> mh.getHandler(request.method()));
requestHandled = dispatchRequest(request, channel, client, threadContext, mHandler);
requestHandled = dispatchRequest(request, channel, client, mHandler);
if (requestHandled) {
break;
}
Expand All @@ -349,6 +358,47 @@ Iterator<MethodHandlers> getAllHandlers(final RestRequest request) {
});
}

/**
* Handle requests to a valid REST endpoint using an unsupported HTTP
* method. A 405 HTTP response code is returned, and the response 'Allow'
* header includes a list of valid HTTP methods for the endpoint (see
* <a href="https://tools.ietf.org/html/rfc2616#section-10.4.6">HTTP/1.1 -
* 10.4.6 - 405 Method Not Allowed</a>).
*/
private void handleUnsupportedHttpMethod(RestRequest request, RestChannel channel, Set<RestRequest.Method> validMethodSet) {
try {
BytesRestResponse bytesRestResponse = BytesRestResponse.createSimpleErrorResponse(channel, METHOD_NOT_ALLOWED,
"Incorrect HTTP method for uri [" + request.uri() + "] and method [" + request.method() + "], allowed: " + validMethodSet);
bytesRestResponse.addHeader("Allow", Strings.collectionToDelimitedString(validMethodSet, ","));
channel.sendResponse(bytesRestResponse);
} catch (final IOException e) {
logger.warn("failed to send bad request response", e);
channel.sendResponse(new BytesRestResponse(INTERNAL_SERVER_ERROR, BytesRestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY));
}
}

/**
* Handle HTTP OPTIONS requests to a valid REST endpoint. A 200 HTTP
* response code is returned, and the response 'Allow' header includes a
* list of valid HTTP methods for the endpoint (see
* <a href="https://tools.ietf.org/html/rfc2616#section-9.2">HTTP/1.1 - 9.2
* - Options</a>).
*/
private void handleOptionsRequest(RestRequest request, RestChannel channel, Set<RestRequest.Method> validMethodSet) {
if (request.method() == RestRequest.Method.OPTIONS && validMethodSet.size() > 0) {
BytesRestResponse bytesRestResponse = new BytesRestResponse(OK, TEXT_CONTENT_TYPE, BytesArray.EMPTY);
bytesRestResponse.addHeader("Allow", Strings.collectionToDelimitedString(validMethodSet, ","));
channel.sendResponse(bytesRestResponse);
} else if (request.method() == RestRequest.Method.OPTIONS && validMethodSet.size() == 0) {
/*
* When we have an OPTIONS HTTP request and no valid handlers,
* simply send OK by default (with the Access Control Origin header
* which gets automatically added).
*/
channel.sendResponse(new BytesRestResponse(OK, TEXT_CONTENT_TYPE, BytesArray.EMPTY));
}
}

/**
* Handle a requests with no candidate handlers (return a 400 Bad Request
* error).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,7 @@ public void testRestHandlerWrapper() throws Exception {
final RestController restController = new RestController(Settings.EMPTY, Collections.emptySet(), wrapper, null,
circuitBreakerService, usageService);
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
restController.dispatchRequest(new FakeRestRequest.Builder(xContentRegistry()).build(),
null, null, threadContext, Optional.of(handler));
restController.dispatchRequest(new FakeRestRequest.Builder(xContentRegistry()).build(), null, null, Optional.of(handler));
assertTrue(wrapperCalled.get());
assertFalse(handlerCalled.get());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.rest;

import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.indices.breaker.CircuitBreakerService;
import org.elasticsearch.indices.breaker.HierarchyCircuitBreakerService;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.rest.FakeRestChannel;
import org.elasticsearch.test.rest.FakeRestRequest;
import org.elasticsearch.usage.UsageService;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.mock;

public class RestHttpResponseHeadersTests extends ESTestCase {

/**
* For requests to a valid REST endpoint using an unsupported HTTP method,
* verify that a 405 HTTP response code is returned, and that the response
* 'Allow' header includes a list of valid HTTP methods for the endpoint
* (see
* <a href="https://tools.ietf.org/html/rfc2616#section-10.4.6">HTTP/1.1 -
* 10.4.6 - 405 Method Not Allowed</a>).
*/
public void testUnsupportedMethodResponseHttpHeader() throws Exception {

/*
* Generate a random set of candidate valid HTTP methods to register
* with the test RestController endpoint. Enums are returned in the
* order they are declared, so the first step is to shuffle the HTTP
* method list, passing in the RandomizedContext's Random instance,
* before picking out a candidate sublist.
*/
List<RestRequest.Method> validHttpMethodArray = new ArrayList<RestRequest.Method>(Arrays.asList(RestRequest.Method.values()));
validHttpMethodArray.remove(RestRequest.Method.OPTIONS);
Collections.shuffle(validHttpMethodArray, random());

/*
* The upper bound of the potential sublist is one less than the size of
* the array, so we are guaranteed at least one invalid method to test.
*/
validHttpMethodArray = validHttpMethodArray.subList(0, randomIntBetween(1, validHttpMethodArray.size() - 1));
assert(validHttpMethodArray.size() > 0);
assert(validHttpMethodArray.size() < RestRequest.Method.values().length);

/*
* Generate an inverse list of one or more candidate invalid HTTP
* methods, so we have a candidate method to fire at the test endpoint.
*/
List<RestRequest.Method> invalidHttpMethodArray = new ArrayList<RestRequest.Method>(Arrays.asList(RestRequest.Method.values()));
invalidHttpMethodArray.removeAll(validHttpMethodArray);
// Remove OPTIONS, or else we'll get a 200 instead of 405
invalidHttpMethodArray.remove(RestRequest.Method.OPTIONS);
assert(invalidHttpMethodArray.size() > 0);

// Initialize test candidate RestController
CircuitBreakerService circuitBreakerService = new HierarchyCircuitBreakerService(Settings.EMPTY,
new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS));

final Settings settings = Settings.EMPTY;
UsageService usageService = new UsageService(settings);
RestController restController = new RestController(settings, Collections.emptySet(),
null, null, circuitBreakerService, usageService);

// A basic RestHandler handles requests to the endpoint
RestHandler restHandler = new RestHandler() {

@Override
public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception {
channel.sendResponse(new TestResponse());
}

};

// Register valid test handlers with test RestController
for (RestRequest.Method method : validHttpMethodArray) {
restController.registerHandler(method, "/", restHandler);
}

// Generate a test request with an invalid HTTP method
FakeRestRequest.Builder fakeRestRequestBuilder = new FakeRestRequest.Builder(xContentRegistry());
fakeRestRequestBuilder.withMethod(invalidHttpMethodArray.get(0));
RestRequest restRequest = fakeRestRequestBuilder.build();

// Send the request and verify the response status code
FakeRestChannel restChannel = new FakeRestChannel(restRequest, false, 1);
NodeClient client = mock(NodeClient.class);
restController.dispatchRequest(restRequest, restChannel, new ThreadContext(Settings.EMPTY));
assertThat(restChannel.capturedResponse().status().getStatus(), is(405));

/*
* Verify the response allow header contains the valid methods for the
* test endpoint
*/
assertThat(restChannel.capturedResponse().getHeaders().get("Allow"), notNullValue());
String responseAllowHeader = restChannel.capturedResponse().getHeaders().get("Allow").get(0);
List<String> responseAllowHeaderArray = Arrays.asList(responseAllowHeader.split(","));
assertThat(responseAllowHeaderArray.size(), is(validHttpMethodArray.size()));
assertThat(responseAllowHeaderArray, containsInAnyOrder(getMethodNameStringArray(validHttpMethodArray).toArray()));
}

private static class TestResponse extends RestResponse {

@Override
public String contentType() {
return null;
}

@Override
public BytesReference content() {
return null;
}

@Override
public RestStatus status() {
return RestStatus.OK;
}

}

/**
* Convert an RestRequest.Method array to a String array, so it can be
* compared with the expected 'Allow' header String array.
*/
private List<String> getMethodNameStringArray(List<RestRequest.Method> methodArray) {
return methodArray.stream().map(method -> method.toString()).collect(Collectors.toList());
}

}
35 changes: 33 additions & 2 deletions docs/reference/migration/migrate_6_0/rest.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ In previous versions of Elasticsearch, delete by query requests without an expli
were accepted, match_all was used as the default query and all documents were deleted
as a result. From version 6.0.0, delete by query requests require an explicit query.

=== DELETE document calls now implicitly create the type
==== DELETE document calls now implicitly create the type

Running `DELETE index/type/id` now implicitly creates `type` with a default
mapping if it did not exist yet.
Expand All @@ -76,8 +76,39 @@ removed.. `GET /_all` can be used to retrieve all aliases, settings, and
mappings for all indices. In order to retrieve only the mappings for an index,
`GET /myindex/_mappings` (or `_aliases`, or `_settings`).

==== Requests to existing endpoints with incorrect HTTP verb now return 405 responses

Issuing a request to an endpoint that exists, but with an incorrect HTTP verb
(such as a `POST` request to `/myindex/_settings`) now returns an HTTP 405
response instead of a 404. An `Allow` header is added to the 405 responses
containing the allowed verbs. For example:

[source,text]
-------------------------------------------
$ curl -v -XPOST 'localhost:9200/my_index/_settings'
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9200 (#0)
> POST /my_index/_settings HTTP/1.1
> Host: localhost:9200
> User-Agent: curl/7.51.0
> Accept: */*
>
< HTTP/1.1 405 Method Not Allowed
< Allow: PUT,GET
< content-type: application/json; charset=UTF-8
< content-length: 134
<
{
"error" : "Incorrect HTTP method for uri [/my_index/_settings] and method [POST], allowed: [PUT, GET]",
"status" : 405
}
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
--------------------------------------------

==== Dissallow using `_cache` and `_cache_key`

The `_cache` and `_cache_key` options in queries have been deprecated since version 2.0.0 and
have been ignored since then, issuing a deprecation warning. These options have now been completely
removed, so using them now will throw an error.
removed, so using them now will throw an error.
Loading