Repository: graphhopper/graphhopper
Pull request #1390 (2018-06-02 14:26:36)

Exception Mapping
Based on #1389, moved the creation of exception responses to where it belongs (in Dropwizard's opinion): To an exception mapper and a message body writer, and a regular JSON serializer for the JSON case. _Those_ can easily be shared by other Repos (map-matching, matrix).

Dropwizard/Jersey want the switch for content types to be _mainly_ done based on the header, not on a query param, but other things are also possible: "re-mapping" or "normalizing" those things in a Filter is what's recommended. (Jersey has an example where the same is done for filename extensions. That's where I copied this from.)

The explanation how it works is in ``GraphHopperBundle``, where it's plugged together:

```
        // If the "?format=gpx" parameter is present, sets a corresponding media type header
        environment.jersey().register(new FormatGPXFilter());

        // Together, these two take care that MultiExceptions thrown from RouteResource
        // come out as JSON or GPX, depending on the media type
        environment.jersey().register(new MultiExceptionMapper());
        environment.jersey().register(new MultiExceptionGPXMessageBodyWriter());
```

michaz: ..and the only thing which complicates doing the same thing for the regular case (the successful response) is that they are parameterized (by "do we want instructions?" and such). Not sure how that is supposed to be done yet.
michaz: If we like this, we can codify in the same way the GraphHopper-convention (correct me if I'm wrong) that IllegalArgumentExceptions should correspond to status 4xx. Then we don't have to wrap them in WebApplicationExceptions (as before) or MultiExceptions (as now) but can throw them directly, even from the library (if it's only one).
Nakaner: I lack the in-depth knowledge of Dropwizard etc. to review the code but I checked out your branch and tested it.

Is it intended that the `type` parameter of the query string is ignored and only the HTTP `Accept` header field (defaults to JSON) matters? Valid requests would return GPX data if `type=gpx` is represent but no `Accept` header field.

`curl  -H "Accept: application/gpx+xml" "localhost:8989/route?point=40.0,30.0&point=20.0,30.0&vehicle=car&format=gpx"` results in an XML error response but if I omit the HTTP `Accept` header field, JSON is returned.

The HTTP API documentation don't mention at all that you need to send a header.
michaz: > Is it intended that the type parameter of the query string is ignored

Err.. no. Hold on..
michaz: Ahh, look, it's supposed to be `&type=gpx`, not `&format=gpx`. Then it works.

Were you primed by my class name ``FormatGPXFilter`` maybe? No idea why I called it that, will change.
karussell: > IllegalArgumentExceptions should correspond to status 400

:+1: 

Patch:
diff --git a/.travis.yml b/.travis.yml
index a6a8dcb34..ac6a9d011 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,10 +11,11 @@ env:
 
 matrix:
   include:
-    - jdk: oraclejdk8
     - jdk: openjdk8
     - env: JDK='OpenJDK 10'
       install: . ./install-jdk.sh -F 10 -C
+    - env: JDK='OpenJDK 11'
+      install: . ./install-jdk.sh -F 11 -C
     
 # avoid default dependency command for maven, 'true' means 'return true' and continue
 install: true
diff --git a/api/src/main/java/com/graphhopper/GHRequest.java b/api/src/main/java/com/graphhopper/GHRequest.java
index 326ea176e..38b40c252 100644
--- a/api/src/main/java/com/graphhopper/GHRequest.java
+++ b/api/src/main/java/com/graphhopper/GHRequest.java
@@ -251,7 +251,7 @@ public class GHRequest {
     }
 
     public boolean hasPointHints() {
-        return pointHints.size() == points.size();
+        return pointHints.size() == points.size() && !points.isEmpty();
     }
 
     public GHRequest setPathDetails(List<String> pathDetails) {
diff --git a/api/src/main/java/com/graphhopper/util/PointList.java b/api/src/main/java/com/graphhopper/util/PointList.java
index 8788410b5..4891a5278 100644
--- a/api/src/main/java/com/graphhopper/util/PointList.java
+++ b/api/src/main/java/com/graphhopper/util/PointList.java
@@ -122,11 +122,6 @@ public class PointList implements Iterable<GHPoint3D>, PointAccess {
             throw new UnsupportedOperationException("cannot access EMPTY PointList");
         }
 
-        @Override
-        public LineString toLineString(boolean includeElevation) {
-            throw new UnsupportedOperationException("cannot access EMPTY PointList");
-        }
-
         @Override
         public void reverse() {
             throw new UnsupportedOperationException("cannot change EMPTY PointList");
diff --git a/client-hc/src/main/java/com/graphhopper/api/GHMRequest.java b/client-hc/src/main/java/com/graphhopper/api/GHMRequest.java
index e12881b34..469e46072 100644
--- a/client-hc/src/main/java/com/graphhopper/api/GHMRequest.java
+++ b/client-hc/src/main/java/com/graphhopper/api/GHMRequest.java
@@ -15,6 +15,8 @@ public class GHMRequest extends GHRequest {
 
     private List<GHPoint> fromPoints;
     private List<GHPoint> toPoints;
+    private List<String> fromPointHints;
+    private List<String> toPointHints;
     boolean identicalLists = true;
     private final Set<String> outArrays = new HashSet<>(5);
 
@@ -26,6 +28,8 @@ public class GHMRequest extends GHRequest {
         super(0);
         fromPoints = new ArrayList<>(size);
         toPoints = new ArrayList<>(size);
+        fromPointHints = new ArrayList<>(size);
+        toPointHints = new ArrayList<>(size);
     }
 
     /**
@@ -51,7 +55,7 @@ public class GHMRequest extends GHRequest {
 
     @Override
     public List<GHPoint> getPoints() {
-        throw new IllegalStateException("use getFromPlaces or getToPlaces");
+        throw new IllegalStateException("use getFromPoints or getToPoints");
     }
 
     public List<GHPoint> getFromPoints() {
@@ -63,7 +67,7 @@ public class GHMRequest extends GHRequest {
     }
 
     /**
-     * This methods adds the places as 'from' and 'to' place to the request.
+     * This methods adds the coordinate as 'from' and 'to' to the request.
      */
     @Override
     public GHMRequest addPoint(GHPoint point) {
@@ -78,21 +82,68 @@ public class GHMRequest extends GHRequest {
         return this;
     }
 
-    public GHMRequest addFromPoints(List<GHPoint> points) {
+    public GHMRequest setFromPoints(List<GHPoint> points) {
         fromPoints = points;
         identicalLists = false;
         return this;
     }
 
+    public GHRequest addFromPointHint(String pointHint) {
+        this.fromPointHints.add(pointHint);
+        return this;
+    }
+
+    public GHRequest setFromPointHints(List<String> pointHints) {
+        this.fromPointHints = pointHints;
+        return this;
+    }
+
+    public List<String> getFromPointHints() {
+        return fromPointHints;
+    }
+
     public GHMRequest addToPoint(GHPoint point) {
         toPoints.add(point);
         identicalLists = false;
         return this;
     }
 
-    public GHMRequest addToPoints(List<GHPoint> points) {
+    public GHMRequest setToPoints(List<GHPoint> points) {
         toPoints = points;
         identicalLists = false;
         return this;
     }
+
+    public GHRequest addToPointHint(String pointHint) {
+        this.toPointHints.add(pointHint);
+        return this;
+    }
+
+    public GHRequest setToPointHints(List<String> pointHints) {
+        this.toPointHints = pointHints;
+        return this;
+    }
+
+    public List<String> getToPointHints() {
+        return toPointHints;
+    }
+
+    @Override
+    public GHRequest setPointHints(List<String> pointHints) {
+        super.setPointHints(pointHints);
+        this.fromPointHints = pointHints;
+        this.toPointHints = pointHints;
+        return this;
+    }
+
+    @Override
+    public List<String> getPointHints() {
+        throw new IllegalStateException("Use getFromPointHints or getToPointHints");
+    }
+
+    @Override
+    public boolean hasPointHints() {
+        return this.fromPointHints.size() == this.fromPoints.size() && !fromPoints.isEmpty() &&
+                this.toPointHints.size() == this.toPoints.size() && !toPoints.isEmpty();
+    }
 }
diff --git a/client-hc/src/main/java/com/graphhopper/api/GHMatrixBatchRequester.java b/client-hc/src/main/java/com/graphhopper/api/GHMatrixBatchRequester.java
index 9cb31ea57..43a219b73 100644
--- a/client-hc/src/main/java/com/graphhopper/api/GHMatrixBatchRequester.java
+++ b/client-hc/src/main/java/com/graphhopper/api/GHMatrixBatchRequester.java
@@ -55,8 +55,6 @@ public class GHMatrixBatchRequester extends GHMatrixAbstractRequester {
     @Override
     public MatrixResponse route(GHMRequest ghRequest) {
         ObjectNode requestJson = factory.objectNode();
-        ArrayNode fromPointList = createPointList(ghRequest.getFromPoints());
-        ArrayNode toPointList = createPointList(ghRequest.getToPoints());
 
         List<String> outArraysList = new ArrayList<>(ghRequest.getOutArrays());
         if (outArraysList.isEmpty()) {
@@ -69,8 +67,18 @@ public class GHMatrixBatchRequester extends GHMatrixAbstractRequester {
         }
 
         boolean hasElevation = false;
-        requestJson.putArray("from_points").addAll(fromPointList);
-        requestJson.putArray("to_points").addAll(toPointList);
+        if (ghRequest.identicalLists) {
+            requestJson.putArray("points").addAll(createPointList(ghRequest.getFromPoints()));
+            requestJson.putArray("point_hints").addAll(createStringList(ghRequest.getFromPointHints()));
+        } else {
+            ArrayNode fromPointList = createPointList(ghRequest.getFromPoints());
+            ArrayNode toPointList = createPointList(ghRequest.getToPoints());
+            requestJson.putArray("from_points").addAll(fromPointList);
+            requestJson.putArray("from_point_hints").addAll(createStringList(ghRequest.getFromPointHints()));
+            requestJson.putArray("to_points").addAll(toPointList);
+            requestJson.putArray("to_point_hints").addAll(createStringList(ghRequest.getToPointHints()));
+        }
+
         requestJson.putArray("out_arrays").addAll(outArrayListJson);
         requestJson.put("vehicle", ghRequest.getVehicle());
         requestJson.put("elevation", hasElevation);
@@ -102,7 +110,7 @@ public class GHMatrixBatchRequester extends GHMatrixAbstractRequester {
 
             JsonNode responseJson = toJSON(postUrl, postResponseStr);
             if (responseJson.has("message")) {
-                matrixResponse.addError(new RuntimeException(responseJson.get("message").asText()));
+                matrixResponse.addErrors(readErrors(responseJson));
                 return matrixResponse;
             }
             if (!responseJson.has("job_id")) {
@@ -167,8 +175,16 @@ public class GHMatrixBatchRequester extends GHMatrixAbstractRequester {
         return matrixResponse;
     }
 
+    private final ArrayNode createStringList(List<String> list) {
+        ArrayNode outList = factory.arrayNode();
+        for (String str : list) {
+            outList.add(str);
+        }
+        return outList;
+    }
+
     protected final ArrayNode createPointList(List<GHPoint> list) {
-        ArrayNode outList = factory.arrayNode(); // new ArrayList<>(list.size())
+        ArrayNode outList = factory.arrayNode();
         for (GHPoint p : list) {
             ArrayNode entry = factory.arrayNode();
             entry.add(p.lon);
diff --git a/client-hc/src/main/java/com/graphhopper/api/GHMatrixSyncRequester.java b/client-hc/src/main/java/com/graphhopper/api/GHMatrixSyncRequester.java
index f535934f8..1761f05c5 100644
--- a/client-hc/src/main/java/com/graphhopper/api/GHMatrixSyncRequester.java
+++ b/client-hc/src/main/java/com/graphhopper/api/GHMatrixSyncRequester.java
@@ -36,18 +36,39 @@ public class GHMatrixSyncRequester extends GHMatrixAbstractRequester {
         ignoreSet.add("vehicle");
         ignoreSet.add("point");
         ignoreSet.add("from_point");
+        ignoreSet.add("from_point_hint");
         ignoreSet.add("to_point");
+        ignoreSet.add("to_point_hint");
         ignoreSet.add("add_array");
     }
 
     @Override
     public MatrixResponse route(GHMRequest ghRequest) {
+        StringBuilder pointHintsStr = new StringBuilder();
+
         String pointsStr;
         if (ghRequest.identicalLists) {
             pointsStr = createPointQuery(ghRequest.getFromPoints(), "point");
+
+            for (String hint : ghRequest.getFromPointHints()) {
+                if (pointHintsStr.length() > 0)
+                    pointHintsStr.append("&");
+                pointHintsStr.append("point_hint=").append(encode(hint));
+            }
         } else {
             pointsStr = createPointQuery(ghRequest.getFromPoints(), "from_point");
             pointsStr += "&" + createPointQuery(ghRequest.getToPoints(), "to_point");
+
+            for (String hint : ghRequest.getFromPointHints()) {
+                if (pointHintsStr.length() > 0)
+                    pointHintsStr.append("&");
+                pointHintsStr.append("from_point_hint=").append(encode(hint));
+            }
+            for (String hint : ghRequest.getToPointHints()) {
+                if (pointHintsStr.length() > 0)
+                    pointHintsStr.append("&");
+                pointHintsStr.append("to_point_hint=").append(encode(hint));
+            }
         }
 
         String outArrayStr = "";
@@ -65,7 +86,7 @@ public class GHMatrixSyncRequester extends GHMatrixAbstractRequester {
         }
 
         String url = buildURL("", ghRequest);
-        url += "&" + pointsStr + "&" + outArrayStr + "&vehicle=" + ghRequest.getVehicle();
+        url += "&" + pointsStr + "&" + pointHintsStr + "&" + outArrayStr + "&vehicle=" + ghRequest.getVehicle();
 
         boolean withTimes = outArraysList.contains("times");
         boolean withDistances = outArraysList.contains("distances");
@@ -94,14 +115,13 @@ public class GHMatrixSyncRequester extends GHMatrixAbstractRequester {
     }
 
     private String createPointQuery(List<GHPoint> list, String pointName) {
-        String pointsStr = "";
+        StringBuilder pointsStr = new StringBuilder();
         for (GHPoint p : list) {
-            if (!pointsStr.isEmpty()) {
-                pointsStr += "&";
-            }
+            if (pointsStr.length() > 0)
+                pointsStr.append("&");
 
-            pointsStr += pointName + "=" + encode(Helper.round6(p.lat) + "," + Helper.round6(p.lon));
+            pointsStr.append(pointName).append('=').append(encode(Helper.round6(p.lat) + "," + Helper.round6(p.lon)));
         }
-        return pointsStr;
+        return pointsStr.toString();
     }
 }
diff --git a/client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java b/client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java
index 525766a53..7253f480d 100644
--- a/client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java
+++ b/client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java
@@ -128,7 +128,7 @@ public class GraphHopperWeb implements GraphHopperAPI {
             JsonNode descriptionNode = path.get("description");
             if (descriptionNode.isArray()) {
                 List<String> description = new ArrayList<>(descriptionNode.size());
-                for (JsonNode descNode: descriptionNode) {
+                for (JsonNode descNode : descriptionNode) {
                     description.add(descNode.asText());
                 }
                 pathWrapper.setDescription(description);
@@ -431,6 +431,10 @@ public class GraphHopperWeb implements GraphHopperAPI {
             url += "&" + Parameters.DETAILS.PATH_DETAILS + "=" + details;
         }
 
+        for (String hint : request.getPointHints()) {
+            url += "&point_hint=" + WebHelper.encodeURL(hint);
+        }
+
         if (!key.isEmpty()) {
             url += "&key=" + WebHelper.encodeURL(key);
         }
diff --git a/core/src/main/java/com/graphhopper/routing/template/ViaRoutingTemplate.java b/core/src/main/java/com/graphhopper/routing/template/ViaRoutingTemplate.java
index dc8e80488..9df3034e1 100644
--- a/core/src/main/java/com/graphhopper/routing/template/ViaRoutingTemplate.java
+++ b/core/src/main/java/com/graphhopper/routing/template/ViaRoutingTemplate.java
@@ -63,19 +63,15 @@ public class ViaRoutingTemplate extends AbstractRoutingTemplate implements Routi
         queryResults = new ArrayList<>(points.size());
         for (int placeIndex = 0; placeIndex < points.size(); placeIndex++) {
             GHPoint point = points.get(placeIndex);
-            QueryResult res;
-            if (ghRequest.hasPointHints()) {
-                res = locationIndex.findClosest(point.lat, point.lon, new NameSimilarityEdgeFilter(edgeFilter, ghRequest.getPointHints().get(placeIndex)));
-                if (!res.isValid()) {
-                    res = locationIndex.findClosest(point.lat, point.lon, edgeFilter);
-                }
-            } else {
-                res = locationIndex.findClosest(point.lat, point.lon, edgeFilter);
-            }
-            if (!res.isValid())
+            QueryResult qr = null;
+            if (ghRequest.hasPointHints())
+                qr = locationIndex.findClosest(point.lat, point.lon, new NameSimilarityEdgeFilter(edgeFilter, ghRequest.getPointHints().get(placeIndex)));
+            if (qr == null || !qr.isValid())
+                qr = locationIndex.findClosest(point.lat, point.lon, edgeFilter);
+            if (!qr.isValid())
                 ghResponse.addError(new PointNotFoundException("Cannot find point " + placeIndex + ": " + point, placeIndex));
 
-            queryResults.add(res);
+            queryResults.add(qr);
         }
 
         return queryResults;
diff --git a/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java b/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
index f95e5a108..90b64a75c 100644
--- a/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
+++ b/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
@@ -44,7 +44,7 @@ import static com.graphhopper.util.Helper.toLowerCase;
  */
 public class EncodingManager {
     private static final String ERR = "Encoders are requesting %s bits, more than %s bits of %s flags. ";
-    private static final String WAY_ERR = "Decrease the number of vehicles or increase the flags to take long via graph.bytes_for_flags=8";
+    private static final String WAY_ERR = "Decrease the number of vehicles or increase the flags to take long via graph.bytes_for_flags: 8";
     private final List<AbstractFlagEncoder> edgeEncoders = new ArrayList<>();
     private final int bitsForEdgeFlags;
     private final int bitsForTurnFlags = 8 * 4;
diff --git a/docs/core/quickstart-from-source.md b/docs/core/quickstart-from-source.md
index 2352ddc1d..bf3b18886 100644
--- a/docs/core/quickstart-from-source.md
+++ b/docs/core/quickstart-from-source.md
@@ -30,6 +30,12 @@ $ ./graphhopper.sh -a web -i north-america_us_new-york.pbf -o new-york-gh
 # the format follows the link structure at http://download.geofabrik.de
 ```
 
+For larger maps you might need to allow the JVM to access more memory. For example for 2GB you can do this using:
+```bash
+$ export JAVA_OPTS="-Xmx=2g -Xms=2g"
+```
+before running `graphhopper.sh`.
+
 ## Start Development
 
 Open the project in your IDE, first class IDEs are NetBeans and IntelliJ where no further setup is required.
diff --git a/docs/web/quickstart.md b/docs/web/quickstart.md
index 45acff304..ac47f822b 100644
--- a/docs/web/quickstart.md
+++ b/docs/web/quickstart.md
@@ -6,7 +6,10 @@ The following steps are simpler and only need the JRE, a jar file and an OSM fil
  1. Install the latest JRE and get the [GraphHopper Web Service](https://github.com/graphhopper/graphhopper/blob/master/README.md#get-started)
 as jar file
  2. Copy an OSM file to the same directory. For example [berlin-latest.osm.pbf](http://download.geofabrik.de/europe/germany/berlin.html)
- 3. Start GraphHopper Maps via: `java -Dgraphhopper.datareader.file=berlin-latest.osm.pbf -jar *.jar server config-example.yml`
+ 3. Start GraphHopper Maps via: 
+    ```bash
+    java -Dgraphhopper.datareader.file=berlin-latest.osm.pbf -jar *.jar server config-example.yml
+    ```
  4. After you see 'Started server at HTTP 8989' go to [http://localhost:8989/](http://localhost:8989/) and you should see a map of Berlin. You should be able to click on the map and a route appears.
 
 See [here](./../core/elevation.md) how to easily enable elevation data. To see how GraphHopper is configured for production usage, see the [deployment guide](./../core/deploy.md).
@@ -17,4 +20,6 @@ See [here](./../core/elevation.md) how to easily enable elevation data. To see h
  * Regarding step 2:
     * The folder where you execute the java command should contain the following files: berlin-latest.osm.pbf, config-example.yml and `graphhopper-web-[version].jar`
     * The first time you execute this it'll take ~30 seconds (for Berlin), further starts will only load the graph and should be nearly instantaneous. You should see log statements but no exceptions and the last entry should be something like: Started server at HTTP 8989
+ * Regarding step 3:
+    * Depending on the size of the map you might run into `java.lang.OutOfMemoryError`. In this case you need to increase the memory settings of the JVM by starting the above command with `java -Xmx2g -Xms2g ...` (example for 2GB memory)
  * Or [contact us](../index.md#contact)
diff --git a/reader-gtfs/src/main/java/com/graphhopper/reader/gtfs/GraphHopperGtfs.java b/reader-gtfs/src/main/java/com/graphhopper/reader/gtfs/GraphHopperGtfs.java
index 05e063f30..675ad98e4 100644
--- a/reader-gtfs/src/main/java/com/graphhopper/reader/gtfs/GraphHopperGtfs.java
+++ b/reader-gtfs/src/main/java/com/graphhopper/reader/gtfs/GraphHopperGtfs.java
@@ -18,6 +18,8 @@
 
 package com.graphhopper.reader.gtfs;
 
+import com.conveyal.gtfs.GTFSFeed;
+import com.conveyal.gtfs.model.Transfer;
 import com.google.transit.realtime.GtfsRealtime;
 import com.graphhopper.*;
 import com.graphhopper.reader.osm.OSMReader;
@@ -343,8 +345,18 @@ public final class GraphHopperGtfs implements GraphHopperAPI {
             } else {
                 walkNetworkIndex = new EmptyLocationIndex();
             }
+            GraphHopperGtfs graphHopperGtfs = new GraphHopperGtfs(ptFlagEncoder, createTranslationMap(), graphHopperStorage, walkNetworkIndex, gtfsStorage, RealtimeFeed.empty(gtfsStorage));
             for (int i = 0; i < id; i++) {
-                new GtfsReader("gtfs_" + i, graphHopperStorage, gtfsStorage, ptFlagEncoder, walkNetworkIndex).readGraph();
+                GTFSFeed gtfsFeed = gtfsStorage.getGtfsFeeds().get("gtfs_" + i);
+                GtfsReader gtfsReader = new GtfsReader("gtfs_" + i, graphHopperStorage, gtfsStorage, ptFlagEncoder, walkNetworkIndex);
+                gtfsReader.connectStopsToStreetNetwork();
+                graphHopperGtfs.getType0TransferWithTimes(gtfsFeed)
+                        .forEach(t -> {
+                            t.transfer.transfer_type = 2;
+                            t.transfer.min_transfer_time = (int) (t.time / 1000L);
+                            gtfsFeed.transfers.put(t.id, t.transfer);
+                        });
+                gtfsReader.readGraph();
             }
             graphHopperStorage.flush();
             return graphHopperStorage;
@@ -371,6 +383,46 @@ public final class GraphHopperGtfs implements GraphHopperAPI {
         return new RequestHandler(request).route();
     }
 
+    private class TransferWithTime {
+        public String id;
+        Transfer transfer;
+        long time;
+    }
+
+    private Stream<TransferWithTime> getType0TransferWithTimes(GTFSFeed gtfsFeed) {
+        return gtfsFeed.transfers.entrySet()
+                .parallelStream()
+                .filter(e -> e.getValue().transfer_type == 0)
+                .map(e -> {
+                    PointList points = new PointList(2, false);
+                    final int fromnode = gtfsStorage.getStationNodes().get(e.getValue().from_stop_id);
+                    final QueryResult fromstation = new QueryResult(graphHopperStorage.getNodeAccess().getLat(fromnode), graphHopperStorage.getNodeAccess().getLon(fromnode));
+                    fromstation.setClosestNode(fromnode);
+                    points.add(graphHopperStorage.getNodeAccess().getLat(fromnode), graphHopperStorage.getNodeAccess().getLon(fromnode));
+
+                    final int tonode = gtfsStorage.getStationNodes().get(e.getValue().to_stop_id);
+                    final QueryResult tostation = new QueryResult(graphHopperStorage.getNodeAccess().getLat(tonode), graphHopperStorage.getNodeAccess().getLon(tonode));
+                    tostation.setClosestNode(tonode);
+                    points.add(graphHopperStorage.getNodeAccess().getLat(tonode), graphHopperStorage.getNodeAccess().getLon(tonode));
+
+                    PtTravelTimeWeighting weighting = createPtTravelTimeWeighting(flagEncoder, false, 5.0);
+                    QueryGraph queryGraph = new QueryGraph(graphHopperStorage);
+                    queryGraph.lookup(Collections.emptyList());
+                    final GraphExplorer graphExplorer = new GraphExplorer(queryGraph, weighting, flagEncoder, gtfsStorage, realtimeFeed, false, Collections.emptyList(), true);
+
+                    MultiCriteriaLabelSetting router = new MultiCriteriaLabelSetting(graphExplorer, weighting, false, Double.MAX_VALUE, Double.MAX_VALUE, false, false, Integer.MAX_VALUE);
+                    final Stream<Label> labels = router.calcLabels(fromnode, tonode, Instant.ofEpochMilli(0), 0);
+                    List<Label> solutions = labels
+                            .filter(current -> tonode == current.adjNode)
+                            .collect(Collectors.toList());
+                    TransferWithTime transferWithTime = new TransferWithTime();
+                    transferWithTime.id = e.getKey();
+                    transferWithTime.transfer = e.getValue();
+                    transferWithTime.time = solutions.get(0).currentTime;
+                    return transferWithTime;
+                });
+    }
+
     private static PtTravelTimeWeighting createPtTravelTimeWeighting(PtFlagEncoder encoder, boolean arriveBy, double walkSpeedKmH) {
         PtTravelTimeWeighting weighting = new PtTravelTimeWeighting(encoder, walkSpeedKmH);
         if (arriveBy) {
diff --git a/reader-gtfs/src/main/java/com/graphhopper/reader/gtfs/GtfsReader.java b/reader-gtfs/src/main/java/com/graphhopper/reader/gtfs/GtfsReader.java
index 16b879738..9168571e3 100644
--- a/reader-gtfs/src/main/java/com/graphhopper/reader/gtfs/GtfsReader.java
+++ b/reader-gtfs/src/main/java/com/graphhopper/reader/gtfs/GtfsReader.java
@@ -138,11 +138,10 @@ class GtfsReader {
         gtfsStorage.getFares().putAll(feed.fares);
         transfers = new Transfers(feed);
         gtfsStorage.getTransfers().put(id, transfers);
-        connectStopsToStreetNetwork();
         buildPtNetwork();
     }
 
-    private void connectStopsToStreetNetwork() {
+    void connectStopsToStreetNetwork() {
         EdgeFilter filter = new EverythingButPt(encoder);
         for (Stop stop : feed.stops.values()) {
             QueryResult locationQueryResult = walkNetworkIndex.findClosest(stop.stop_lat, stop.stop_lon, filter);
diff --git a/web-api/src/main/java/com/graphhopper/MultiException.java b/web-api/src/main/java/com/graphhopper/MultiException.java
new file mode 100644
index 000000000..6b59ce982
--- /dev/null
+++ b/web-api/src/main/java/com/graphhopper/MultiException.java
@@ -0,0 +1,40 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH 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 com.graphhopper;
+
+import java.util.Collections;
+import java.util.List;
+
+public class MultiException extends RuntimeException {
+
+    private final List<Throwable> errors;
+
+    public MultiException(List<Throwable> errors) {
+        this.errors = errors;
+    }
+
+    public MultiException(Throwable e) {
+        this(Collections.singletonList(e));
+    }
+
+    public List<Throwable> getErrors() {
+        return errors;
+    }
+
+}
diff --git a/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java b/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java
index ac45eeb1e..c26a46e27 100644
--- a/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java
+++ b/web-api/src/main/java/com/graphhopper/jackson/GraphHopperModule.java
@@ -1,6 +1,7 @@
 package com.graphhopper.jackson;
 
 import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.graphhopper.MultiException;
 import com.graphhopper.util.CmdArgs;
 import com.graphhopper.util.InstructionList;
 import com.graphhopper.util.details.PathDetail;
@@ -18,6 +19,7 @@ public class GraphHopperModule extends SimpleModule {
         addSerializer(PathDetail.class, new PathDetailSerializer());
         addSerializer(InstructionList.class, new InstructionListSerializer());
         addDeserializer(CmdArgs.class, new CmdArgsDeserializer());
+        addSerializer(MultiException.class, new MultiExceptionSerializer());
     }
 
 }
diff --git a/web-api/src/main/java/com/graphhopper/jackson/MultiExceptionSerializer.java b/web-api/src/main/java/com/graphhopper/jackson/MultiExceptionSerializer.java
new file mode 100644
index 000000000..b7798eb9a
--- /dev/null
+++ b/web-api/src/main/java/com/graphhopper/jackson/MultiExceptionSerializer.java
@@ -0,0 +1,59 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH 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 com.graphhopper.jackson;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.graphhopper.MultiException;
+import com.graphhopper.util.exceptions.GHException;
+
+import java.io.IOException;
+import java.util.List;
+
+public class MultiExceptionSerializer extends JsonSerializer<MultiException> {
+
+    @Override
+    public void serialize(MultiException e, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
+        List<Throwable> errors = e.getErrors();
+        ObjectNode json = JsonNodeFactory.instance.objectNode();
+        json.put("message", getMessage(errors.get(0)));
+        ArrayNode errorHintList = json.putArray("hints");
+        for (Throwable t : errors) {
+            ObjectNode error = errorHintList.addObject();
+            error.put("message", getMessage(t));
+            error.put("details", t.getClass().getName());
+            if (t instanceof GHException) {
+                ((GHException) t).getDetails().forEach(error::putPOJO);
+            }
+        }
+        jsonGenerator.writeObject(json);
+    }
+
+    private static String getMessage(Throwable t) {
+        if (t.getMessage() == null)
+            return t.getClass().getSimpleName();
+        else
+            return t.getMessage();
+    }
+
+}
diff --git a/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java b/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java
index e59e6ed75..104eb3663 100644
--- a/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java
+++ b/web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java
@@ -183,6 +183,14 @@ public class GraphHopperBundle implements ConfiguredBundle<GraphHopperBundleConf
     public void run(GraphHopperBundleConfiguration configuration, Environment environment) {
         configuration.getGraphHopperConfiguration().merge(CmdArgs.readFromSystemProperties());
 
+        // If the "?type=gpx" parameter is present, sets a corresponding media type header
+        environment.jersey().register(new TypeGPXFilter());
+
+        // Together, these two take care that MultiExceptions thrown from RouteResource
+        // come out as JSON or GPX, depending on the media type
+        environment.jersey().register(new MultiExceptionMapper());
+        environment.jersey().register(new MultiExceptionGPXMessageBodyWriter());
+
         if (configuration.getGraphHopperConfiguration().has("gtfs.file")) {
             // switch to different API implementation when using Pt
             runPtGraphHopper(configuration.getGraphHopperConfiguration(), environment);
@@ -263,7 +271,6 @@ public class GraphHopperBundle implements ConfiguredBundle<GraphHopperBundleConf
         environment.jersey().register(IsochroneResource.class);
         environment.jersey().register(I18NResource.class);
         environment.jersey().register(InfoResource.class);
-
         environment.healthChecks().register("graphhopper", new GraphHopperHealthCheck(graphHopperManaged.getGraphHopper()));
     }
 
diff --git a/web-bundle/src/main/java/com/graphhopper/http/MultiExceptionGPXMessageBodyWriter.java b/web-bundle/src/main/java/com/graphhopper/http/MultiExceptionGPXMessageBodyWriter.java
new file mode 100644
index 000000000..2f53c1210
--- /dev/null
+++ b/web-bundle/src/main/java/com/graphhopper/http/MultiExceptionGPXMessageBodyWriter.java
@@ -0,0 +1,99 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH 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 com.graphhopper.http;
+
+import com.graphhopper.MultiException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+@Provider
+@Produces("application/gpx+xml")
+public class MultiExceptionGPXMessageBodyWriter implements MessageBodyWriter<MultiException> {
+
+    @Override
+    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return true;
+    }
+
+    @Override
+    public long getSize(MultiException e, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return -1;
+    }
+
+    @Override
+    public void writeTo(MultiException e, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
+        if (e.getErrors().isEmpty())
+            throw new RuntimeException("errorsToXML should not be called with an empty list");
+
+        try {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document doc = builder.newDocument();
+            Element gpxElement = doc.createElement("gpx");
+            gpxElement.setAttribute("creator", "GraphHopper");
+            gpxElement.setAttribute("version", "1.1");
+            doc.appendChild(gpxElement);
+
+            Element mdElement = doc.createElement("metadata");
+            gpxElement.appendChild(mdElement);
+
+            Element extensionsElement = doc.createElement("extensions");
+            mdElement.appendChild(extensionsElement);
+
+            Element messageElement = doc.createElement("message");
+            extensionsElement.appendChild(messageElement);
+            messageElement.setTextContent(e.getErrors().iterator().next().getMessage());
+
+            Element hintsElement = doc.createElement("hints");
+            extensionsElement.appendChild(hintsElement);
+
+            for (Throwable t : e.getErrors()) {
+                Element error = doc.createElement("error");
+                hintsElement.appendChild(error);
+                error.setAttribute("message", t.getMessage());
+                error.setAttribute("details", t.getClass().getName());
+            }
+            TransformerFactory transformerFactory = TransformerFactory.newInstance();
+            Transformer transformer = transformerFactory.newTransformer();
+            transformer.transform(new DOMSource(doc), new StreamResult(entityStream));
+        } catch (ParserConfigurationException | TransformerException e2) {
+            throw new RuntimeException(e2);
+        }
+
+    }
+}
diff --git a/web-bundle/src/main/java/com/graphhopper/http/MultiExceptionMapper.java b/web-bundle/src/main/java/com/graphhopper/http/MultiExceptionMapper.java
new file mode 100644
index 000000000..237daee61
--- /dev/null
+++ b/web-bundle/src/main/java/com/graphhopper/http/MultiExceptionMapper.java
@@ -0,0 +1,33 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH 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 com.graphhopper.http;
+
+import com.graphhopper.MultiException;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Provider
+public class MultiExceptionMapper implements ExceptionMapper<MultiException> {
+    @Override
+    public Response toResponse(MultiException exception) {
+        return Response.status(400).entity(exception).build();
+    }
+}
diff --git a/web-bundle/src/main/java/com/graphhopper/http/TypeGPXFilter.java b/web-bundle/src/main/java/com/graphhopper/http/TypeGPXFilter.java
new file mode 100644
index 000000000..7c38043c3
--- /dev/null
+++ b/web-bundle/src/main/java/com/graphhopper/http/TypeGPXFilter.java
@@ -0,0 +1,40 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH 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 com.graphhopper.http;
+
+import javax.annotation.Priority;
+import javax.ws.rs.Priorities;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.PreMatching;
+import javax.ws.rs.core.HttpHeaders;
+
+@PreMatching
+@Priority(Priorities.HEADER_DECORATOR)
+public class TypeGPXFilter implements ContainerRequestFilter {
+
+    @Override
+    public void filter(ContainerRequestContext rc) {
+        String maybeType = rc.getUriInfo().getQueryParameters().getFirst("type");
+        if (maybeType != null && maybeType.equals("gpx")) {
+            rc.getHeaders().putSingle(HttpHeaders.ACCEPT, "application/gpx+xml");
+        }
+    }
+
+}
diff --git a/web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java b/web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java
index 3314fa81f..aca3bdbfc 100644
--- a/web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java
+++ b/web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java
@@ -17,33 +17,26 @@
  */
 package com.graphhopper.resources;
 
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.graphhopper.GHRequest;
 import com.graphhopper.GHResponse;
 import com.graphhopper.GraphHopperAPI;
+import com.graphhopper.MultiException;
 import com.graphhopper.http.WebHelper;
 import com.graphhopper.routing.util.EncodingManager;
 import com.graphhopper.routing.util.HintsMap;
 import com.graphhopper.util.Constants;
 import com.graphhopper.util.Parameters;
 import com.graphhopper.util.StopWatch;
-import com.graphhopper.util.exceptions.GHException;
 import com.graphhopper.util.shapes.GHPoint;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.*;
+import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.core.*;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
 import java.util.*;
 
 import static com.graphhopper.util.Parameters.Routing.*;
@@ -76,7 +69,8 @@ public class RouteResource {
     public Response doGet(
             @Context HttpServletRequest httpReq,
             @Context UriInfo uriInfo,
-            @QueryParam(WAY_POINT_MAX_DISTANCE) @DefaultValue("1") double minPathPrecision,
+            @Context ContainerRequestContext rc,
+            @QueryParam(WAY_POINT_MAX_DISTANCE)@DefaultValue("1") double minPathPrecision,
             @QueryParam("point") List<GHPoint> requestPoints,
             @QueryParam("type") @DefaultValue("json") String type,
             @QueryParam(INSTRUCTIONS) @DefaultValue("true") boolean instructions,
@@ -101,20 +95,20 @@ public class RouteResource {
         StopWatch sw = new StopWatch().start();
 
         if(requestPoints.isEmpty()) {
-            throw new WebApplicationException(errorResponse(new IllegalArgumentException("You have to pass at least one point"), writeGPX));
+            throw new MultiException(new IllegalArgumentException("You have to pass at least one point"));
         }
 
         if (!encodingManager.supports(vehicleStr)) {
-            throw new WebApplicationException(errorResponse(new IllegalArgumentException("Vehicle not supported: " + vehicleStr), writeGPX));
+            throw new MultiException(new IllegalArgumentException("Vehicle not supported: " + vehicleStr));
         } else if (enableElevation && !hasElevation) {
-            throw new WebApplicationException(errorResponse(new IllegalArgumentException("Elevation not supported!"), writeGPX));
+            throw new MultiException(new IllegalArgumentException("Elevation not supported!"));
         } else if (favoredHeadings.size() > 1 && favoredHeadings.size() != requestPoints.size()) {
-            throw new WebApplicationException(errorResponse(new IllegalArgumentException("The number of 'heading' parameters must be <= 1 "
-                    + "or equal to the number of points (" + requestPoints.size() + ")"), writeGPX));
+            throw new MultiException(new IllegalArgumentException("The number of 'heading' parameters must be <= 1 "
+                    + "or equal to the number of points (" + requestPoints.size() + ")"));
         }
 
         if (pointHints.size() > 0 && pointHints.size() != requestPoints.size()) {
-            throw new WebApplicationException(errorResponse(new IllegalArgumentException("If you pass " + POINT_HINT + ", you need to pass a hint for every point, empty hints will be ignored"), writeGPX));
+            throw new MultiException(new IllegalArgumentException("If you pass " + POINT_HINT + ", you need to pass a hint for every point, empty hints will be ignored"));
         }
 
         GHRequest request;
@@ -153,7 +147,7 @@ public class RouteResource {
 
         if (ghResponse.hasErrors()) {
             logger.error(logStr + ", errors:" + ghResponse.getErrors());
-            throw new WebApplicationException(errorResponse(ghResponse.getErrors(), writeGPX));
+            throw new MultiException(ghResponse.getErrors());
         } else {
             logger.info(logStr + ", alternatives: " + ghResponse.getAll().size()
                     + ", distance0: " + ghResponse.getBest().getDistance()
@@ -173,7 +167,7 @@ public class RouteResource {
 
     private static Response.ResponseBuilder gpxSuccessResponseBuilder(GHResponse ghRsp, String timeString, String trackName, boolean enableElevation, boolean withRoute, boolean withTrack, boolean withWayPoints, String version) {
         if (ghRsp.getAll().size() > 1) {
-            throw new WebApplicationException("Alternatives are currently not yet supported for GPX");
+            throw new MultiException(new IllegalArgumentException("Alternatives are currently not yet supported for GPX"));
         }
 
         long time = timeString != null ? Long.parseLong(timeString) : System.currentTimeMillis();
@@ -181,72 +175,7 @@ public class RouteResource {
                 header("Content-Disposition", "attachment;filename=" + "GraphHopper.gpx");
     }
 
-    private static Response xmlErrorResponse(Collection<Throwable> list) {
-        if (list.isEmpty())
-            throw new RuntimeException("errorsToXML should not be called with an empty list");
-
-        try {
-            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-            DocumentBuilder builder = factory.newDocumentBuilder();
-            Document doc = builder.newDocument();
-            Element gpxElement = doc.createElement("gpx");
-            gpxElement.setAttribute("creator", "GraphHopper");
-            gpxElement.setAttribute("version", "1.1");
-            doc.appendChild(gpxElement);
-
-            Element mdElement = doc.createElement("metadata");
-            gpxElement.appendChild(mdElement);
-
-            Element extensionsElement = doc.createElement("extensions");
-            mdElement.appendChild(extensionsElement);
-
-            Element messageElement = doc.createElement("message");
-            extensionsElement.appendChild(messageElement);
-            messageElement.setTextContent(list.iterator().next().getMessage());
-
-            Element hintsElement = doc.createElement("hints");
-            extensionsElement.appendChild(hintsElement);
-
-            for (Throwable t : list) {
-                Element error = doc.createElement("error");
-                hintsElement.appendChild(error);
-                error.setAttribute("message", t.getMessage());
-                error.setAttribute("details", t.getClass().getName());
-            }
-            return Response.status(400).entity(doc).build();
-        } catch (ParserConfigurationException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static Response errorResponse(List<Throwable> t, boolean writeGPX) {
-        if (writeGPX) {
-            return xmlErrorResponse(t);
-        } else {
-            return jsonErrorResponse(t);
-        }
-    }
-
-    private static Response errorResponse(Throwable t, boolean writeGPX) {
-        return errorResponse(Collections.singletonList(t), writeGPX);
-    }
-
-    private static Response jsonErrorResponse(List<Throwable> errors) {
-        ObjectNode json = JsonNodeFactory.instance.objectNode();
-        json.put("message", getMessage(errors.get(0)));
-        ArrayNode errorHintList = json.putArray("hints");
-        for (Throwable t : errors) {
-            ObjectNode error = errorHintList.addObject();
-            error.put("message", getMessage(t));
-            error.put("details", t.getClass().getName());
-            if (t instanceof GHException) {
-                ((GHException) t).getDetails().forEach(error::putPOJO);
-            }
-        }
-        return Response.status(400).entity(json).build();
-    }
-
-    public static void initHints(HintsMap m, MultivaluedMap<String, String> parameterMap) {
+    static void initHints(HintsMap m, MultivaluedMap<String, String> parameterMap) {
         for (Map.Entry<String, List<String>> e : parameterMap.entrySet()) {
             if (e.getValue().size() == 1) {
                 m.put(e.getKey(), e.getValue().get(0));
@@ -267,11 +196,4 @@ public class RouteResource {
         }
     }
 
-    private static String getMessage(Throwable t) {
-        if (t.getMessage() == null)
-            return t.getClass().getSimpleName();
-        else
-            return t.getMessage();
-    }
-
 }
diff --git a/web/pom.xml b/web/pom.xml
index e5685e990..c9e48ef4b 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -20,6 +20,11 @@
     </properties>
 
     <dependencies>
+        <dependency>
+            <groupId>javax.activation</groupId>
+            <artifactId>activation</artifactId>
+            <version>1.1.1</version>
+        </dependency>
         <dependency>
             <groupId>io.dropwizard</groupId>
             <artifactId>dropwizard-core</artifactId>
diff --git a/web/src/main/java/com/graphhopper/http/GraphHopperApplication.java b/web/src/main/java/com/graphhopper/http/GraphHopperApplication.java
index 7035bb772..825343644 100644
--- a/web/src/main/java/com/graphhopper/http/GraphHopperApplication.java
+++ b/web/src/main/java/com/graphhopper/http/GraphHopperApplication.java
@@ -17,19 +17,8 @@
  */
 package com.graphhopper.http;
 
-import com.bedatadriven.jackson.datatype.jts.JtsModule;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.databind.BeanDescription;
-import com.fasterxml.jackson.databind.SerializationConfig;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
-import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
-import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
 import com.graphhopper.http.cli.ImportCommand;
 import com.graphhopper.http.resources.RootResource;
-import com.graphhopper.jackson.GraphHopperModule;
 import io.dropwizard.Application;
 import io.dropwizard.bundles.assets.ConfiguredAssetsBundle;
 import io.dropwizard.setup.Bootstrap;
@@ -37,8 +26,6 @@ import io.dropwizard.setup.Environment;
 
 import javax.servlet.DispatcherType;
 import java.util.EnumSet;
-import java.util.List;
-import java.util.stream.Collectors;
 
 public final class GraphHopperApplication extends Application<GraphHopperServerConfiguration> {
 
@@ -55,7 +42,6 @@ public final class GraphHopperApplication extends Application<GraphHopperServerC
 
     @Override
     public void run(GraphHopperServerConfiguration configuration, Environment environment) throws Exception {
-
         environment.jersey().register(new RootResource());
         environment.servlets().addFilter("cors", CORSFilter.class).addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, "*");
         environment.servlets().addFilter("ipfilter", new IPFilter(configuration.getGraphHopperConfiguration().get("jetty.whiteips", ""), configuration.getGraphHopperConfiguration().get("jetty.blackips", ""))).addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, "*");


Test patch:
diff --git a/api/src/test/java/com/graphhopper/util/PointListTest.java b/api/src/test/java/com/graphhopper/util/PointListTest.java
index 4df09a0a4..f401da7cb 100644
--- a/api/src/test/java/com/graphhopper/util/PointListTest.java
+++ b/api/src/test/java/com/graphhopper/util/PointListTest.java
@@ -237,6 +237,8 @@ public class PointListTest {
 
         assertEquals(3, pl.toLineString(true).getNumPoints());
         assertEquals(2, pl.shallowCopy(1, 3, false).toLineString(true).getNumPoints());
+
+        assertEquals(0, PointList.EMPTY.toLineString(false).getNumPoints());
     }
 
 
diff --git a/client-hc/src/test/java/com/graphhopper/api/AbstractGraphHopperMatrixWebIntegrationTester.java b/client-hc/src/test/java/com/graphhopper/api/AbstractGraphHopperMatrixWebIntegrationTester.java
index 8d175d509..2b5e819f4 100644
--- a/client-hc/src/test/java/com/graphhopper/api/AbstractGraphHopperMatrixWebIntegrationTester.java
+++ b/client-hc/src/test/java/com/graphhopper/api/AbstractGraphHopperMatrixWebIntegrationTester.java
@@ -6,6 +6,7 @@ import org.junit.Test;
 
 import java.util.Arrays;
 
+import static com.graphhopper.api.GraphHopperMatrixWeb.SERVICE_URL;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -65,10 +66,40 @@ public abstract class AbstractGraphHopperMatrixWebIntegrationTester {
         ghmRequest.addOutArray("distances");
         ghmRequest.addOutArray("times");
         ghmRequest.setVehicle("car");
-        ghmRequest.addFromPoints(Arrays.asList(new GHPoint(52.557151, 13.515244)))
-                .addToPoints(Arrays.asList(new GHPoint(52.557151, 13.515244), new GHPoint(52.454545, 13.295517)));
+        ghmRequest.setFromPoints(Arrays.asList(new GHPoint(52.557151, 13.515244)))
+                .setToPoints(Arrays.asList(new GHPoint(52.557151, 13.515244), new GHPoint(52.454545, 13.295517)));
 
         MatrixResponse res = ghMatrix.route(ghmRequest);
         assertEquals(2437, res.getTime(0, 1) / 1000, 30);
     }
+
+    @Test
+    public void testPOSTMatrixQueryWithPointHints() {
+        GHMRequest req = new GHMRequest();
+        req.addPoint(new GHPoint(52.517004, 13.389416));
+        req.addPoint(new GHPoint(52.485707, 13.435249));
+        req.addPoint(new GHPoint(52.516848, 13.424606));
+        req.addOutArray("distances");
+        MatrixResponse res = ghMatrix.route(req);
+        assertEquals(4833, res.getDistance(1, 2), 50);
+
+        req = new GHMRequest();
+        req.addPoint(new GHPoint(52.517004, 13.389416));
+        req.addPoint(new GHPoint(52.485707, 13.435249));
+        req.addPoint(new GHPoint(52.516848, 13.424606));
+        req.addOutArray("distances");
+        req.setPointHints(Arrays.asList("", "singerstr", ""));
+        res = ghMatrix.route(req);
+        assertEquals(3900, res.getDistance(1, 2), 50);
+
+        req = new GHMRequest();
+        req.addPoint(new GHPoint(52.517004, 13.389416));
+        req.addPoint(new GHPoint(52.485707, 13.435249));
+        req.addPoint(new GHPoint(52.516848, 13.424606));
+        // wrong count
+        req.setPointHints(Arrays.asList("", "singerstr"));
+        res = ghMatrix.route(req);
+        assertTrue(res.hasErrors());
+        assertEquals("Array length of point_hints must match length of points (or from/to equivalent)", res.getErrors().get(0).getMessage());
+    }
 }
diff --git a/client-hc/src/test/java/com/graphhopper/api/GraphHopperMatrixGoogleIT.java b/client-hc/src/test/java/com/graphhopper/api/GraphHopperMatrixGoogleIT.java
index f205cd9d5..0b664d271 100644
--- a/client-hc/src/test/java/com/graphhopper/api/GraphHopperMatrixGoogleIT.java
+++ b/client-hc/src/test/java/com/graphhopper/api/GraphHopperMatrixGoogleIT.java
@@ -32,4 +32,9 @@ public class GraphHopperMatrixGoogleIT {
         assertEquals(11000, res.getDistance(1, 2), 2000);
         assertEquals(2500, res.getTime(1, 2) / 1000, 1000);
     }
+
+    @Test
+    public void testPOSTMatrixQueryWithPointHints() {
+        // for now ignore
+    }
 }
diff --git a/client-hc/src/test/java/com/graphhopper/api/GraphHopperWebIT.java b/client-hc/src/test/java/com/graphhopper/api/GraphHopperWebIT.java
index 012c0e8a2..df6a5fad6 100644
--- a/client-hc/src/test/java/com/graphhopper/api/GraphHopperWebIT.java
+++ b/client-hc/src/test/java/com/graphhopper/api/GraphHopperWebIT.java
@@ -169,7 +169,6 @@ public class GraphHopperWebIT {
         assertTrue(res.getErrors().get(0) instanceof PointNotFoundException);
     }
 
-
     @Test
     public void testOutOfBoundsException() {
         GHRequest req = new GHRequest().
@@ -332,4 +331,15 @@ public class GraphHopperWebIT {
         assertTrue((Double) details.get(0).getValue() > 20);
         assertTrue((Double) details.get(0).getValue() < 70);
     }
+
+    @Test
+    public void testPointHints() {
+        GHRequest ghRequest = new GHRequest();
+        ghRequest.addPoint(new GHPoint(52.50977, 13.371971));
+        ghRequest.addPoint(new GHPoint(52.509842, 13.369761));
+
+        ghRequest.setPointHints(Arrays.asList("Ben-Gurion", ""));
+        GHResponse response = gh.route(ghRequest);
+        assertTrue(response.getBest().getDistance() + "m", response.getBest().getDistance() < 500);
+    }
 }