diff --git framework/base/src/main/java/org/apache/ofbiz/base/util/collections/MultivaluedMapContext.java framework/base/src/main/java/org/apache/ofbiz/base/util/collections/MultivaluedMapContext.java
new file mode 100644
index 0000000000..d31bfba4a6
--- /dev/null
+++ framework/base/src/main/java/org/apache/ofbiz/base/util/collections/MultivaluedMapContext.java
@@ -0,0 +1,249 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.base.util.collections;
+
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.ofbiz.base.util.UtilGenerics;
+
+/**
+ * MultivaluedMap Stack
+ *
+ */
+public class MultivaluedMapContext<K, V> implements MultivaluedMap<K, V>, LocalizedMap<V> {
+
+    public static final String module = MultivaluedMapContext.class.getName();
+
+    public static final <K, V> MultivaluedMapContext<K, V> getMapContext() {
+        return new MultivaluedMapContext<>();
+    }
+
+    public static <K, V> MultivaluedMapContext<K, V> createMapContext() {
+        MultivaluedMapContext<K, V> newValue = MultivaluedMapContext.getMapContext();
+        // initialize with a single entry
+        newValue.push();
+        return newValue;
+    }
+
+    public static <K, V> MultivaluedMapContext<K, V> createMapContext(MultivaluedMap<K, V> baseMap) {
+        if (baseMap instanceof MultivaluedMapContext) {
+            return createMapContext((MultivaluedMapContext<K, V>) baseMap);
+        } else {
+            MultivaluedMapContext<K, V> newValue = MultivaluedMapContext.getMapContext();
+            newValue.maps.addFirst(baseMap);
+            return newValue;
+        }
+    }
+
+    /** Does a shallow copy of the internal stack of the passed MapContext; enables simultaneous stacks that share common parent Maps */
+    public static <K, V> MultivaluedMapContext<K, V> createMapContext(MultivaluedMapContext<K, V> source) {
+        MultivaluedMapContext<K, V> newValue = MultivaluedMapContext.getMapContext();
+        newValue.maps.addAll(source.maps);
+        return newValue;
+    }
+
+    protected MultivaluedMapContext() {
+        super();
+    }
+
+    protected Deque<MultivaluedMap<K, V>> maps = new LinkedList<>();
+
+    public void reset() {
+        maps = new LinkedList<>();
+    }
+
+    /** Puts a new Map on the top of the stack */
+    public void push() {
+        MultivaluedMap<K, V> newMap = new MultivaluedHashMap<>();
+        this.maps.addFirst(newMap);
+    }
+
+    /** Puts an existing Map on the top of the stack (top meaning will override lower layers on the stack) */
+    public void push(MultivaluedMap<K, V> existingMap) {
+        if (existingMap == null) {
+            throw new IllegalArgumentException("Error: cannot push null existing Map onto a MapContext");
+        }
+        this.maps.addFirst(existingMap);
+    }
+
+    /** Puts an existing Map on the BOTTOM of the stack (bottom meaning will be overriden by lower layers on the stack, ie everything else already there) */
+    public void addToBottom(MultivaluedMap<K, V> existingMap) {
+        if (existingMap == null) {
+            throw new IllegalArgumentException("Error: cannot add null existing Map to bottom of a MapContext");
+        }
+        this.maps.add(existingMap);
+    }
+
+    /** Remove and returns the Map from the top of the stack; if there is only one Map on the stack it returns null and does not remove it */
+    public MultivaluedMap<K, V> pop() {
+        // always leave at least one Map in the List, ie never pop off the last Map
+        return (maps.size() < 1) ? null : maps.removeFirst();
+    }
+
+    /**
+     * Creates a MapContext object that has the same Map objects on its stack;
+     * meant to be used to enable a
+     * situation where a parent and child context are operating simultaneously
+     * using two different MapContext objects, but sharing the Maps in common
+     */
+    public MultivaluedMapContext<K, V> standAloneStack() {
+        MultivaluedMapContext<K, V> standAlone = MultivaluedMapContext.createMapContext(this);
+        return standAlone;
+    }
+
+    /**
+     * Creates a MapContext object that has the same Map objects on its stack,
+     * but with a new Map pushed on the top; meant to be used to enable a
+     * situation where a parent and child context are operating simultaneously
+     * using two different MapContext objects, but sharing the Maps in common
+     */
+    public MultivaluedMapContext<K, V> standAloneChildStack() {
+        MultivaluedMapContext<K, V> standAloneChild = MultivaluedMapContext.createMapContext(this);
+        standAloneChild.push();
+        return standAloneChild;
+    }
+
+    @Override
+    public int size() {
+        return keySet().size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return maps.stream().allMatch(MultivaluedMap::isEmpty);
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return maps.stream().anyMatch(map -> map.containsKey(key));
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return maps.stream().anyMatch(map -> map.containsValue(value));
+    }
+
+    @Override
+    public List<V> get(Object key) {
+        return maps.stream()
+                   .filter(m -> m.containsKey(key))
+                   .flatMap(m -> m.get(key).stream())
+                   .collect(Collectors.toList());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public V get(String name, Locale locale) {
+        for (MultivaluedMap<K, V> curMap: maps) {
+            // only return if the curMap contains the key, rather than checking for null; this allows a null at a lower level to override a value at a higher level
+            if (curMap.containsKey(name)) {
+                if (curMap instanceof LocalizedMap<?>) {
+                    LocalizedMap<V> lmap = UtilGenerics.cast(curMap);
+                    return lmap.get(name, locale);
+                }
+                return curMap.get((K) name).stream().findFirst().get();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public List<V> remove(Object key) {
+        return maps.getFirst().remove(key);
+    }
+
+    @Override
+    public void clear() {
+        maps.getFirst().clear();
+    }
+
+    // Convert an iterator to a stream.
+    private static <U> Stream<U> stream(Iterator<U> it) {
+        Iterable<U> dmaps = () -> it;
+        return StreamSupport.stream(dmaps.spliterator(), false);
+    }
+
+    @Override
+    public Set<K> keySet() {
+        // Collect in reverse order to let the first maps masks the next ones.
+        return stream(maps.descendingIterator())
+                .flatMap(m -> m.keySet().stream())
+                .collect(Collectors.toCollection(HashSet::new));
+    }
+
+    @Override
+    public List<V> put(K key, List<V> value) {
+        return maps.getFirst().put(key, value);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends List<V>> m) {
+        maps.getFirst().putAll(m);
+    }
+
+    @Override
+    public Set<Entry<K, List<V>>> entrySet() {
+        // Collect in reverse order to let the first maps masks the next ones.
+        return stream(maps.descendingIterator())
+                .flatMap(m -> m.entrySet().stream())
+                .collect(Collectors.toCollection(HashSet::new));
+    }
+
+    @Override
+    public void putSingle(K key, V value) {
+        maps.getFirst().putSingle(key, value);
+    }
+
+    @Override
+    public void add(K key, V value) {
+        maps.getFirst().add(key, value);
+    }
+
+    @Override
+    public V getFirst(K key) {
+        return maps.stream()
+                   .map(m -> m.get(key))
+                   .filter(Objects::nonNull)
+                   .flatMap(List::stream)
+                   .findFirst()
+                   .orElse(null);
+    }
+
+    @Override
+    public Collection<List<V>> values() {
+        return maps.stream()
+                   .flatMap(m -> m.values().stream())
+                   .collect(Collectors.toList());
+    }
+}
diff --git framework/webapp/config/WebappUiLabels.xml framework/webapp/config/WebappUiLabels.xml
index 474facf187..bcd89a3862 100644
--- framework/webapp/config/WebappUiLabels.xml
+++ framework/webapp/config/WebappUiLabels.xml
@@ -339,4 +339,8 @@
         <value xml:lang="zh">没有完成事件</value>
         <value xml:lang="zh-TW">沒有完成事件</value>
     </property>
+    <property key="RequestMethodNotMatchConfig">
+        <value xml:lang="en">[{0}] cannot be called by [{1}] method.</value>
+        <value xml:lang="zh">[{0}]不能用[{1}]方法请求。</value>
+    </property>
 </resource>
diff --git framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java
index 6c02513ed5..aa29c247f2 100644
--- framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java
+++ framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java
@@ -23,16 +23,21 @@ import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import javax.servlet.ServletContext;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
 
 import org.apache.ofbiz.base.component.ComponentConfig.WebappInfo;
 import org.apache.ofbiz.base.location.FlexibleLocation;
@@ -49,6 +54,7 @@ import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.base.util.UtilXml;
 import org.apache.ofbiz.base.util.cache.UtilCache;
 import org.apache.ofbiz.base.util.collections.MapContext;
+import org.apache.ofbiz.base.util.collections.MultivaluedMapContext;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
@@ -192,7 +198,7 @@ public class ConfigXMLReader {
         private Map<String, Event> beforeLogoutEventList = new LinkedHashMap<String, Event>();
         private Map<String, String> eventHandlerMap = new HashMap<String, String>();
         private Map<String, String> viewHandlerMap = new HashMap<String, String>();
-        private Map<String, RequestMap> requestMapMap = new HashMap<String, RequestMap>();
+        private MultivaluedMap<String, RequestMap> requestMapMap = new MultivaluedHashMap<>();
         private Map<String, ViewMap> viewMapMap = new HashMap<String, ViewMap>();
 
         public ControllerConfig(URL url) throws WebAppConfigurationException {
@@ -276,11 +282,49 @@ public class ConfigXMLReader {
             return getIncludes(ccfg -> ccfg.protectView);
         }
 
+        // XXX: Temporary wrapper to handle conversion from Map to MultiMap.
         public Map<String, RequestMap> getRequestMapMap() throws WebAppConfigurationException {
-            MapContext<String, RequestMap> result = MapContext.getMapContext();
+            return new Map<String, RequestMap> () {
+                private MultivaluedMap<String, RequestMap> deleg = getRequestMapMultiMap();
+                @Override public int size() { return deleg.size(); }
+                @Override public boolean isEmpty() { return deleg.isEmpty(); }
+                @Override public boolean containsKey(Object key) { return deleg.containsKey(key); }
+                @Override public boolean containsValue(Object value) { return deleg.containsValue(value); }
+                @Override public RequestMap get(Object key) { return deleg.getFirst((String) key); }
+                @Override public RequestMap put(String key, RequestMap value) {
+                    RequestMap res = get(key);
+                    deleg.putSingle(key, value);
+                    return res;
+                }
+                @Override public RequestMap remove(Object key) {
+                    RequestMap res = get(key);
+                    deleg.remove(key);
+                    return res;
+                }
+                @Override public void putAll(Map<? extends String, ? extends RequestMap> m) { m.forEach(deleg::add); }
+                @Override public void clear() { deleg.clear(); }
+                @Override public Set<String> keySet() { return deleg.keySet(); }
+                @Override public Collection<RequestMap> values() {
+                    return deleg.values()
+                                .stream()
+                                .map(m -> m.stream().findFirst().orElse(null))
+                                .filter(Objects::nonNull)
+                                .collect(Collectors.toList());
+                }
+                @Override public Set<Entry<String, RequestMap>> entrySet() {
+                    return deleg.keySet()
+                                .stream()
+                                .collect(Collectors.toMap(k -> k, k -> get(k)))
+                                .entrySet();
+                }
+            };
+        }
+
+        public MultivaluedMap<String, RequestMap> getRequestMapMultiMap() throws WebAppConfigurationException {
+            MultivaluedMapContext<String, RequestMap> result = MultivaluedMapContext.getMapContext();
             for (URL includeLocation : includes) {
                 ControllerConfig controllerConfig = getControllerConfig(includeLocation);
-                result.push(controllerConfig.getRequestMapMap());
+                result.push(controllerConfig.getRequestMapMultiMap());
             }
             result.push(requestMapMap);
             return result;
@@ -403,7 +447,7 @@ public class ConfigXMLReader {
         private void loadRequestMap(Element root) {
             for (Element requestMapElement : UtilXml.childElementList(root, "request-map")) {
                 RequestMap requestMap = new RequestMap(requestMapElement);
-                this.requestMapMap.put(requestMap.uri, requestMap);
+                this.requestMapMap.putSingle(requestMap.uri, requestMap);
             }
         }
 
diff --git framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlServlet.java framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlServlet.java
index d552473234..4d365ceab2 100644
--- framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlServlet.java
+++ framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlServlet.java
@@ -211,6 +211,12 @@ public class ControlServlet extends HttpServlet {
         try {
             // the ServerHitBin call for the event is done inside the doRequest method
             requestHandler.doRequest(request, response, null, userLogin, delegator);
+        } catch (MethodNotAllowedException e) {
+            response.setContentType("text/plain");
+            response.setCharacterEncoding(request.getCharacterEncoding());
+            response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+            response.getWriter().print(e.getMessage());
+            Debug.logError(e.getMessage(), module);
         } catch (RequestHandlerException e) {
             Throwable throwable = e.getNested() != null ? e.getNested() : e;
             if (throwable instanceof IOException) {
diff --git framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/MethodNotAllowedException.java framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/MethodNotAllowedException.java
new file mode 100644
index 0000000000..8e545e27c3
--- /dev/null
+++ framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/MethodNotAllowedException.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.webapp.control;
+
+@SuppressWarnings("serial")
+public class MethodNotAllowedException extends RequestHandlerException {
+    MethodNotAllowedException(String str) {
+        super(str);
+    }
+}
diff --git framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
index b8f70c81dd..ed3e2b6038 100644
--- framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
+++ framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
@@ -24,16 +24,21 @@ import java.io.IOException;
 import java.io.Serializable;
 import java.net.URL;
 import java.security.cert.X509Certificate;
+
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
+import javax.ws.rs.core.MultivaluedMap;
 
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.SSLUtil;
@@ -51,6 +56,7 @@ import org.apache.ofbiz.entity.GenericValue;
 import org.apache.ofbiz.entity.util.EntityQuery;
 import org.apache.ofbiz.entity.util.EntityUtilProperties;
 import org.apache.ofbiz.webapp.OfbizUrlBuilder;
+import org.apache.ofbiz.webapp.control.ConfigXMLReader.RequestMap;
 import org.apache.ofbiz.webapp.event.EventFactory;
 import org.apache.ofbiz.webapp.event.EventHandler;
 import org.apache.ofbiz.webapp.event.EventHandlerException;
@@ -78,7 +84,7 @@ public class RequestHandler {
     private Controller ctrl;
 
     static class Controller {
-        private final Map<String, ConfigXMLReader.RequestMap> requestMapMap;
+        private final MultivaluedMap<String, RequestMap> requestMapMap;
         private final Map<String, ConfigXMLReader.ViewMap> viewMapMap;
         private String statusCodeString;
         private final String defaultRequest;
@@ -90,7 +96,7 @@ public class RequestHandler {
         Controller(ConfigXMLReader.ControllerConfig ccfg) throws WebAppConfigurationException {
             preprocessorEventList = ccfg.getPreprocessorEventList();
             postprocessorEventList = ccfg.getPostprocessorEventList();
-            requestMapMap = ccfg.getRequestMapMap();
+            requestMapMap = ccfg.getRequestMapMultiMap();
             viewMapMap = ccfg.getViewMapMap();
             defaultRequest = ccfg.getDefaultRequest();
             firstVisitEventList = ccfg.getFirstVisitEventList();
@@ -100,7 +106,7 @@ public class RequestHandler {
             statusCodeString = UtilValidate.isEmpty(status) ? defaultStatusCodeString : status;
         }
 
-        public Map<String, ConfigXMLReader.RequestMap> getRequestMapMap() {
+        public MultivaluedMap<String, RequestMap> getRequestMapMap() {
             return requestMapMap;
         }
 
@@ -172,6 +178,56 @@ public class RequestHandler {
         return null;
     }
 
+    /**
+     * Find a collection of request maps in {@code ctrl} matching {@code req}.
+     * Otherwise fall back to matching the {@code defaultReq} field.
+     *
+     * @param ctrl The controller containing the current configuration
+     * @param req The HTTP request to match
+     * @return a collection of request maps which might be empty
+     */
+    static Collection<RequestMap> resolveURI(Controller ctrl, HttpServletRequest req) {
+        Map<String, List<RequestMap>> requestMapMap = ctrl.getRequestMapMap();
+        Map<String, ConfigXMLReader.ViewMap> viewMapMap = ctrl.getViewMapMap();
+        String defaultRequest = ctrl.getDefaultRequest();
+        String path = req.getPathInfo();
+        String requestUri = getRequestUri(path);
+        String viewUri = getOverrideViewUri(path);
+        Collection<RequestMap> rmaps;
+        if (requestMapMap.containsKey(requestUri) && !viewMapMap.containsKey(viewUri)) {
+            rmaps = requestMapMap.get(requestUri);
+        } else if (defaultRequest != null) {
+            rmaps = requestMapMap.get(defaultRequest);
+        } else {
+            rmaps = null;
+        }
+        return rmaps != null ? rmaps : Collections.emptyList();
+    }
+
+    /**
+     * Find the request map matching {@code method}.
+     * Otherwise fall back to the one matching the "all" and "" special methods
+     * in that respective order.
+     *
+     * @param method the HTTP method to match
+     * @param rmaps the collection of request map candidates
+     * @return a request map {@code Optional}
+     */
+    static Optional<RequestMap> resolveMethod(String method, Collection<RequestMap> rmaps) {
+        for (RequestMap map : rmaps) {
+            if (map.method.equalsIgnoreCase(method)) {
+                return Optional.of(map);
+            }
+        }
+        if (method.isEmpty()) {
+            return Optional.empty();
+        } else if (method.equals("all")) {
+            return resolveMethod("", rmaps);
+        } else {
+            return resolveMethod("all", rmaps);
+        }
+    }
+
     public void doRequest(HttpServletRequest request, HttpServletResponse response, String requestUri) throws RequestHandlerException, RequestHandlerExceptionAllowExternalRequests {
         HttpSession session = request.getSession();
         Delegator delegator = (Delegator) request.getAttribute("delegator");
@@ -187,11 +243,6 @@ public class RequestHandler {
         long startTime = System.currentTimeMillis();
         HttpSession session = request.getSession();
 
-        // get the controllerConfig once for this method so we don't have to get it over and over inside the method
-        ConfigXMLReader.ControllerConfig controllerConfig = this.getControllerConfig();
-        Map<String, ConfigXMLReader.RequestMap> requestMapMap = null;
-        String statusCodeString = null;
-
         // Parse controller config.
         try {
             ctrl = new Controller(getControllerConfig());
@@ -213,32 +264,29 @@ public class RequestHandler {
             }
         }
 
-        String overrideViewUri = RequestHandler.getOverrideViewUri(request.getPathInfo());
+        String requestMissingErrorMessage = "Unknown request ["
+                + defaultRequestUri
+                + "]; this request does not exist or cannot be called directly.";
 
-        String requestMissingErrorMessage = "Unknown request [" + defaultRequestUri + "]; this request does not exist or cannot be called directly.";
-        ConfigXMLReader.RequestMap requestMap = null;
-        if (defaultRequestUri != null) {
-            requestMap = ctrl.getRequestMapMap().get(defaultRequestUri);
-        }
-        // check for default request
-        if (requestMap == null && ctrl.getDefaultRequest() != null) {
-            requestMap = ctrl.getRequestMapMap().get(ctrl.getDefaultRequest());
-        }
+        String path = request.getPathInfo();
+        String requestUri = getRequestUri(path);
+        String overrideViewUri = getOverrideViewUri(path);
 
-        // check for override view
-        if (overrideViewUri != null) {
-            ConfigXMLReader.ViewMap viewMap = ctrl.getViewMapMap().get(overrideViewUri);
-            if (viewMap == null && ctrl.getDefaultRequest() != null) {
-                requestMap = ctrl.getRequestMapMap().get(ctrl.getDefaultRequest());
+        Collection<RequestMap> rmaps = resolveURI(ctrl, request);
+        if (rmaps.isEmpty()) {
+            if (throwRequestHandlerExceptionOnMissingLocalRequest) {
+              throw new RequestHandlerException(requestMissingErrorMessage);
+            } else {
+              throw new RequestHandlerExceptionAllowExternalRequests();
             }
         }
 
-        // if no matching request is found in the controller, depending on throwRequestHandlerExceptionOnMissingLocalRequest
-        //  we throw a RequestHandlerException or RequestHandlerExceptionAllowExternalRequests
-        if (requestMap == null) {
-            if (throwRequestHandlerExceptionOnMissingLocalRequest) throw new RequestHandlerException(requestMissingErrorMessage);
-            else throw new RequestHandlerExceptionAllowExternalRequests();
-         }
+        String method = request.getMethod();
+        RequestMap requestMap = resolveMethod(method, rmaps).orElseThrow(() -> {
+            String msg = UtilProperties.getMessage("WebappUiLabels", "RequestMethodNotMatchConfig",
+                    UtilMisc.toList(requestUri, method), UtilHttp.getLocale(request));
+            return new MethodNotAllowedException(msg);
+        });
 
         String eventReturn = null;
         if (requestMap.metrics != null && requestMap.metrics.getThreshold() != 0.0 && requestMap.metrics.getTotalEvents() > 3 && requestMap.metrics.getThreshold() < requestMap.metrics.getServiceRate()) {
@@ -246,13 +294,11 @@ public class RequestHandler {
         }
         ConfigXMLReader.RequestMap originalRequestMap = requestMap; // Save this so we can update the correct performance metrics.
 
-
         boolean interruptRequest = false;
-
         // Check for chained request.
         if (chain != null) {
             String chainRequestUri = RequestHandler.getRequestUri(chain);
-            requestMap = ctrl.getRequestMapMap().get(chainRequestUri);
+            requestMap = ctrl.getRequestMapMap().getFirst(chainRequestUri);
             if (requestMap == null) {
                 throw new RequestHandlerException("Unknown chained request [" + chainRequestUri + "]; this request does not exist");
             }
@@ -276,11 +322,11 @@ public class RequestHandler {
             // Check to make sure we are allowed to access this request directly. (Also checks if this request is defined.)
             // If the request cannot be called, or is not defined, check and see if there is a default-request we can process
             if (!requestMap.securityDirectRequest) {
-                if (ctrl.getDefaultRequest() == null || !ctrl.getRequestMapMap().get(ctrl.getDefaultRequest()).securityDirectRequest) {
+                if (ctrl.getDefaultRequest() == null || !ctrl.getRequestMapMap().getFirst(ctrl.getDefaultRequest()).securityDirectRequest) {
                     // use the same message as if it was missing for security reasons, ie so can't tell if it is missing or direct request is not allowed
                     throw new RequestHandlerException(requestMissingErrorMessage);
                 } else {
-                    requestMap = ctrl.getRequestMapMap().get(ctrl.getDefaultRequest());
+                    requestMap = ctrl.getRequestMapMap().getFirst(ctrl.getDefaultRequest());
                 }
             }
             // Check if we SHOULD be secure and are not.
@@ -433,7 +479,7 @@ public class RequestHandler {
             // Invoke the security handler
             // catch exceptions and throw RequestHandlerException if failed.
             if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler]: AuthRequired. Running security check. " + showSessionId(request), module);
-            ConfigXMLReader.Event checkLoginEvent = ctrl.getRequestMapMap().get("checkLogin").event;
+            ConfigXMLReader.Event checkLoginEvent = ctrl.getRequestMapMap().getFirst("checkLogin").event;
             String checkLoginReturnString = null;
 
             try {
@@ -446,9 +492,9 @@ public class RequestHandler {
                 eventReturn = checkLoginReturnString;
                 // if the request is an ajax request we don't want to return the default login check
                 if (!"XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
-                    requestMap = ctrl.getRequestMapMap().get("checkLogin");
+                    requestMap = ctrl.getRequestMapMap().getFirst("checkLogin");
                 } else {
-                    requestMap = ctrl.getRequestMapMap().get("ajaxCheckLogin");
+                    requestMap = ctrl.getRequestMapMap().getFirst("ajaxCheckLogin");
                 }
             }
         }
diff --git framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/RequestHandlerTests.java framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/RequestHandlerTests.java
new file mode 100644
index 0000000000..e86dd3d41b
--- /dev/null
+++ framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/RequestHandlerTests.java
@@ -0,0 +1,180 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ofbiz.webapp.control;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.hamcrest.CoreMatchers.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.ofbiz.webapp.control.ConfigXMLReader.RequestMap;
+import org.apache.ofbiz.webapp.control.ConfigXMLReader.ViewMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+public class RequestHandlerTests {
+    public static class ResolveURITests {
+        private MultivaluedMap<String,RequestMap> reqMaps;
+        private Map<String, ViewMap> viewMaps;
+        private HttpServletRequest req;
+        private Element dummyElement;
+        private RequestHandler.Controller ctrl;
+
+        @Before
+        public void setUp() {
+            ctrl = mock(RequestHandler.Controller.class);
+            reqMaps = new MultivaluedHashMap<>();
+            viewMaps = new HashMap<>();
+            when(ctrl.getDefaultRequest()).thenReturn(null);
+            when(ctrl.getRequestMapMap()).thenReturn(reqMaps);
+            when(ctrl.getViewMapMap()).thenReturn(viewMaps);
+            req = mock(HttpServletRequest.class);
+            dummyElement = mock(Element.class);
+            when(dummyElement.getAttribute("method")).thenReturn("all");
+            when(req.getMethod()).thenReturn("get");
+        }
+
+        @Test
+        public void resolveURIBasic() throws RequestHandlerException {
+            RequestMap foo = new RequestMap(dummyElement);
+            RequestMap bar = new RequestMap(dummyElement);
+            reqMaps.putSingle("foo", foo);
+            reqMaps.putSingle("bar", bar);
+            when(req.getPathInfo()).thenReturn("/foo");
+            assertThat(RequestHandler.resolveURI(ctrl, req),
+                    both(hasItem(foo)).and(not(hasItem(bar))));
+            assertThat(RequestHandler.resolveURI(ctrl, req).size(), is(1));
+        }
+
+        @Test
+        public void resolveURIBasicPut() throws RequestHandlerException {
+            when(dummyElement.getAttribute("method")).thenReturn("put");
+            when(req.getPathInfo()).thenReturn("/foo");
+            when(req.getMethod()).thenReturn("put");
+
+            RequestMap foo = new RequestMap(dummyElement);
+
+            assertTrue(RequestHandler.resolveURI(ctrl, req).isEmpty());
+            reqMaps.putSingle("foo", foo);
+            assertFalse(RequestHandler.resolveURI(ctrl, req).isEmpty());
+        }
+
+        @Test
+        public void resolveURIUpperCase() throws RequestHandlerException {
+            when(dummyElement.getAttribute("method")).thenReturn("get");
+            RequestMap foo = new RequestMap(dummyElement);
+            when(dummyElement.getAttribute("method")).thenReturn("put");
+            RequestMap bar = new RequestMap(dummyElement);
+            reqMaps.putSingle("foo", foo);
+            reqMaps.putSingle("bar", bar);
+
+            when(req.getPathInfo()).thenReturn("/foo");
+            when(req.getMethod()).thenReturn("GET");
+            assertThat(RequestHandler.resolveURI(ctrl, req), hasItem(foo));
+
+            when(req.getPathInfo()).thenReturn("/bar");
+            when(req.getMethod()).thenReturn("PUT");
+            assertThat(RequestHandler.resolveURI(ctrl, req), hasItem(bar));
+        }
+
+
+        @Test
+        public void resolveURIDefault() throws Exception {
+            RequestMap foo = new RequestMap(dummyElement);
+            RequestMap bar = new RequestMap(dummyElement);
+            reqMaps.putSingle("foo", foo);
+            reqMaps.putSingle("bar", bar);
+
+            when(req.getPathInfo()).thenReturn("/baz");
+            when(ctrl.getDefaultRequest()).thenReturn("bar");
+            assertThat(RequestHandler.resolveURI(ctrl, req), hasItem(bar));
+        }
+
+        @Test
+        public void resolveURIOverrideView() throws Exception {
+            RequestMap foo = new RequestMap(dummyElement);
+            RequestMap bar = new RequestMap(dummyElement);
+            reqMaps.putSingle("foo", foo);
+            reqMaps.putSingle("bar", bar);
+
+            viewMaps.put("baz", new ViewMap(dummyElement));
+
+            when(req.getPathInfo()).thenReturn("/foo/baz");
+            when(ctrl.getDefaultRequest()).thenReturn("bar");
+            assertThat(RequestHandler.resolveURI(ctrl, req), hasItem(bar));
+        }
+
+        @Test
+        public void resolveURINoDefault() throws Exception {
+            RequestMap foo = new RequestMap(dummyElement);
+            RequestMap bar = new RequestMap(dummyElement);
+            reqMaps.putSingle("foo", foo);
+            reqMaps.putSingle("bar", bar);
+
+            when(req.getPathInfo()).thenReturn("/baz");
+            assertTrue(RequestHandler.resolveURI(ctrl, req).isEmpty());
+        }
+
+        @Test
+        public void resolveMethodCatchAll() throws RequestHandlerException {
+            when(req.getPathInfo()).thenReturn("/foo");
+            RequestMap foo = new RequestMap(dummyElement);
+            Collection<RequestMap> rmaps = RequestHandler.resolveURI(ctrl, req);
+            assertFalse(RequestHandler.resolveMethod("get", rmaps).isPresent());
+            assertFalse(RequestHandler.resolveMethod("post", rmaps).isPresent());
+            assertFalse(RequestHandler.resolveMethod("put", rmaps).isPresent());
+            assertFalse(RequestHandler.resolveMethod("delete", rmaps).isPresent());
+
+            reqMaps.putSingle("foo", foo);
+            Collection<RequestMap> rmaps2 = RequestHandler.resolveURI(ctrl, req);
+            assertTrue(RequestHandler.resolveMethod("get", rmaps2).isPresent());
+            assertTrue(RequestHandler.resolveMethod("post", rmaps2).isPresent());
+            assertTrue(RequestHandler.resolveMethod("put", rmaps2).isPresent());
+            assertTrue(RequestHandler.resolveMethod("delete", rmaps2).isPresent());
+        }
+
+        @Test
+        public void resolveMethodBasic() throws RequestHandlerException {
+            when(dummyElement.getAttribute("method")).thenReturn("put");
+            RequestMap fooPut = new RequestMap(dummyElement);
+            when(dummyElement.getAttribute("method")).thenReturn("all");
+            RequestMap fooAll = new RequestMap(dummyElement);
+            reqMaps.putSingle("foo", fooAll);
+            reqMaps.add("foo", fooPut);
+
+            when(req.getPathInfo()).thenReturn("/foo");
+            Collection<RequestMap> rmaps = RequestHandler.resolveURI(ctrl, req);
+            assertThat(rmaps, hasItems(fooPut, fooAll));
+            assertThat(RequestHandler.resolveMethod("put", rmaps).get(), is(fooPut));
+            assertThat(RequestHandler.resolveMethod("get", rmaps).get(), is(fooAll));
+        }
+    }
+}
-- 
2.17.1

