title: Locally Hosted SparkJava Proxy Server Stalls when Querying Google Maps API
body: <p>I am trying to write a proxy server with SparkJava that queries the Google Maps Directions API given parameters (i.e. location data, traffic model preference, departure time, etc...) from a client and returns various routing details such as distance, duration, and duration.</p>
  <p>The server stalls when it tries to send a request to the API on behalf of the client. I placed print statements throughout the code to confirm that the hang was due to the API query. I have tried using different ports namely: <code>4567</code>, <code>443</code>, <code>80</code>, and <code>8080</code> by using <code>port()</code> method but the problem persists. I am sure the server-side code conducting the API query is not the issue; everything works fine (proper route information is generated i.e. <code>DirectionsApiRequest.await()</code> returns properly) when I cut the client out, disable the endpoints, and run everything manually from the main method on the (deactivated) server's side.</p>
  <p>Does anyone know why this could be happening?
  (I use maven for dependency management)</p>
  <p>The following shows the client trying to get the distance of the default route and the aforementioned error:</p>
  <p>Server-side code:</p>
  <p><code>Main class</code></p>
  <pre class="lang-java prettyprint-override"><code>package com.mycompany.app;
  
  //import
  // data structures
  import java.util.ArrayList;
  // google maps
  import com.google.maps.model.DirectionsRoute;
  import com.google.maps.model.LatLng;
  // gson
  import com.google.gson.Gson;
  import com.google.gson.reflect.TypeToken;
  import java.lang.reflect.Type;
  // static API methods
  import com.mycompany.app.DirectionsUtility;
  import static spark.Spark.*;
  // exceptions
  import com.google.maps.errors.ApiException;
  import java.io.IOException;
  
  public class App
  {
      private static ArrayList&lt;LatLng&gt; locationsDatabase = new ArrayList&lt;LatLng&gt;();
      private static DirectionsRoute defaultRoute = null;
  
      public static void main( String[] args ) throws ApiException, InterruptedException, IOException
      {
          // client posts location data
          post(&quot;routingEngine/sendLocations&quot;, (request,response) -&gt; {
              response.type(&quot;application/json&quot;);
              ArrayList&lt;LatLng&gt; locations = new Gson().fromJson(request.body(),new TypeToken&lt;ArrayList&lt;LatLng&gt;&gt;(){}.getType());
              locationsDatabase = locations;
              return &quot;OK&quot;;
          });
  
          // before any default route queries, the default route must be generated
          before(&quot;routingEngine/getDefaultRoute/*&quot;,(request,response) -&gt;{
              RequestParameters requestParameters = new Gson().fromJson(request.body(),(java.lang.reflect.Type)RequestParameters.class);
              defaultRoute = DirectionsUtility.getDefaultRoute(locationsDatabase,requestParameters);
          });
  
          // client gets default route distance
          get(&quot;routingEngine/getDefaultRoute/distance&quot;, (request,response) -&gt;{
              response.type(&quot;application/json&quot;);
              return new Gson().toJson(new Gson().toJson(DirectionsUtility.getDefaultRouteDistance(defaultRoute)));
          });
  
          DirectionsUtility.context.shutdown();
      }
  }
  </code></pre>
  <p><code>DirectionsUtility</code> is the class responsible for consulting with Google Maps' API:</p>
  <pre class="lang-java prettyprint-override"><code>package com.mycompany.app;
  
  // import
  // data structures
  import java.util.List;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Map;
  import java.util.HashMap;
  
  // Google Directions API
  import com.google.maps.GeoApiContext;
  // request parameters
  import com.google.maps.DirectionsApiRequest;
  import com.google.maps.model.Unit;
  import com.google.maps.model.TravelMode;
  import com.google.maps.model.TrafficModel;
  import com.google.maps.DirectionsApi.RouteRestriction;
  import com.google.maps.model.Distance;
  // result parameters
  import com.google.maps.model.DirectionsResult;
  import com.google.maps.model.LatLng;
  import com.google.maps.model.DirectionsRoute;
  import com.google.maps.model.DirectionsLeg;
  // exceptions
  import com.google.maps.errors.ApiException;
  import java.io.IOException;
  // time constructs
  import java.time.Instant;   
  import java.util.concurrent.TimeUnit;
  
  
  import okhttp3.OkHttpClient;
  import okhttp3.Request;
  import okhttp3.RequestBody;
  import okhttp3.Call;
  import okhttp3.Response;
  import okhttp3.MediaType;
  import okhttp3.HttpUrl;
  
  import com.google.gson.Gson;
  import com.google.gson.GsonBuilder;
  
  public final class DirectionsUtility{
  
      /**
      *  Private constructor to prevent instantiation.
      */
      private DirectionsUtility(){}
  
      /**
      *  API key.
      */
      private static final String API_KEY = &quot;YOUR PERSONAL API KEY&quot;;
      
      /**
      *  Queries per second limit (50 is max).
      */
      private static int QPS = 50;
  
      /**
      *  Singleton that facilitates Google Geo API queries; must be shutdown() for program termination.
      */
      protected static GeoApiContext context = new GeoApiContext.Builder()
          .apiKey(API_KEY)
          .queryRateLimit(QPS)
          .build();
  
      // TESTING
      // singleton client
      private static final OkHttpClient httpClient = new OkHttpClient.Builder()
      .connectTimeout(700,TimeUnit.SECONDS)
      .writeTimeout(700, TimeUnit.SECONDS)
      .readTimeout(700, TimeUnit.SECONDS)
      .build();
  
      /**
      *  Generates the route judged by the Google API as being the most optimal. The main purpose of this method is to provide a fallback
      *  for the optimization engine should it ever find the traditional processes of this server (i.e. generation of all possible routes)
      *  too slow for its taste. In other words, if this server delays to an excessive degree in providing the optimization engine with the
      *  set of all possible routes, the optimization engine can terminate those processes and instead entrust the decision to the Google
      *  Maps API. This method suffers from a minor caveat; the Google Maps API refuses to compute the duration in traffic for any journey
      *  involving multiple locations if the intermediate points separating the origin and destination are assumed to be stopover points (i.e.
      *  if it is assumed that the driver will stop at each point) therefore this method assumes that the driver will not stop at the intermediate
      *  points. This may introduce some inaccuracies into the predictions.
      *  (it should be noted that this server has not yet been equipped with the ability to generate all possible routes so this method is, at the
      *  at the moment, the only option)
      *
      *  @param requestParameters the parameters required for a Google Maps API query; see the RequestParameters class for more information
      *
      *  @return the default route
      */
      public static DirectionsRoute getDefaultRoute(ArrayList&lt;LatLng&gt; locations,RequestParameters requestParameters) throws ApiException, InterruptedException, IOException
      {
          LatLng origin = locations.get(0);
          LatLng destination = locations.get(locations.size() - 1);
          // separate waypoints
          int numWaypoints = locations.size() - 2;
          DirectionsApiRequest.Waypoint[] waypoints = new DirectionsApiRequest.Waypoint[numWaypoints];            
          for(int i = 0; i &lt; waypoints.length; i++)
          {
              // ensure that each waypoint is not designated as a stopover point
              waypoints[i] = new DirectionsApiRequest.Waypoint(locations.get(i + 1),false);
          }
          // send API query
          // store API query response
          DirectionsResult directionsResult = null;
          try
          {
              // create DirectionsApiRequest object
              DirectionsApiRequest directionsRequest = new DirectionsApiRequest(context);
              // set request parameters
              directionsRequest.units(requestParameters.getUnit());
              directionsRequest.mode(TravelMode.DRIVING);
              directionsRequest.trafficModel(requestParameters.getTrafficModel());
              if(requestParameters.getRestrictions() != null)
              {
                  directionsRequest.avoid(requestParameters.getRestrictions());
              }
              directionsRequest.region(requestParameters.getRegion());
              directionsRequest.language(requestParameters.getLanguage());
              directionsRequest.departureTime(requestParameters.getDepartureTime());
              // always generate alternative routes
              directionsRequest.alternatives(false);
              directionsRequest.origin(origin);
              directionsRequest.destination(destination);
              directionsRequest.waypoints(waypoints);
              directionsRequest.optimizeWaypoints(requestParameters.optimizeWaypoints());
              // send request and store result
              // testing - notification that a new api query is being sent
              System.out.println(&quot;firing off API query...&quot;);
              directionsResult = directionsRequest.await();
              // testing - notification that api query was successful
              System.out.println(&quot;API query successful&quot;);
  
          }
          catch(Exception e)
          {
              System.out.println(e);
          }
  
          // directionsResult.routes contains only a single, optimized route 
          // return the default route
          return directionsResult.routes[0];
      } // end method
      
      /**
      *  Returns the distance of the default route.
      *  
      *  @param defaultRoute the default route
      *
      *  @return the distance of the default route
      */
      public static Distance getDefaultRouteDistance(DirectionsRoute defaultRoute)
      {
          // testing - simple notification
          System.out.println(&quot;Computing distance...&quot;);
          // each route has only 1 leg since all the waypoints are non-stopover points
          return defaultRoute.legs[0].distance;
      }
  
   }
  </code></pre>
  <p>Here is the client-side code:</p>
  <pre class="lang-java prettyprint-override"><code>package com.mycompany.app;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  
  import com.google.maps.model.LatLng;
  import com.google.maps.model.TrafficModel;
  import com.google.maps.DirectionsApi.RouteRestriction;
  import com.google.maps.model.TransitRoutingPreference;
  import com.google.maps.model.TravelMode;
  import com.google.maps.model.Unit;
  import com.google.gson.Gson;
  import com.google.gson.GsonBuilder;
  import com.google.gson.JsonElement;
  import com.google.gson.JsonArray;
  
  import com.google.gson.reflect.TypeToken;
  import java.lang.reflect.Type;
  
  import okhttp3.OkHttpClient;
  import okhttp3.Request;
  import okhttp3.RequestBody;
  import okhttp3.Call;
  import okhttp3.Response;
  import okhttp3.MediaType;
  import okhttp3.HttpUrl;
  
  // time constructs
  import java.time.LocalDateTime;
  import java.time.Instant;
  import java.time.ZoneOffset;
  import java.util.concurrent.TimeUnit;
  import com.google.maps.model.Distance;
  import com.google.maps.model.Duration;
  
  import java.io.IOException;
  
  public class App 
  {
      // model database
  
      private static LatLng hartford_ct = new LatLng(41.7658,-72.6734);
      private static LatLng loretto_pn = new LatLng(40.5031,-78.6303);
      private static LatLng chicago_il = new LatLng(41.8781,-87.6298);
      private static LatLng newyork_ny = new LatLng(40.7128,-74.0060);
      private static LatLng newport_ri = new LatLng(41.4901,-71.3128);
      private static LatLng concord_ma = new LatLng(42.4604,-71.3489);
      private static LatLng washington_dc = new LatLng(38.8951,-77.0369);
      private static LatLng greensboro_nc = new LatLng(36.0726,-79.7920);
      private static LatLng atlanta_ga = new LatLng(33.7490,-84.3880);
      private static LatLng tampa_fl = new LatLng(27.9506,-82.4572);
  
      // singleton client
      private static final OkHttpClient httpClient = new OkHttpClient.Builder()
      .connectTimeout(700,TimeUnit.SECONDS)
      .writeTimeout(700, TimeUnit.SECONDS)
      .readTimeout(700, TimeUnit.SECONDS)
      .build();
  
  
  
      private static final MediaType JSON
              = MediaType.parse(&quot;application/json; charset=utf-8&quot;);
  
      public static void main( String[] args ) throws IOException
      {
          // post location data
  
          // get locations from database
          ArrayList&lt;LatLng&gt; locations = new ArrayList&lt;LatLng&gt;();
          // origin
          LatLng origin = hartford_ct;
          locations.add(origin);
  
          // waypoints
          locations.add(loretto_pn);
          locations.add(chicago_il);
          locations.add(newyork_ny);
          locations.add(newport_ri);
          locations.add(concord_ma);
          locations.add(washington_dc);
          locations.add(greensboro_nc);
          locations.add(atlanta_ga);
          
          // destination
          LatLng destination = tampa_fl;
          locations.add(destination);
  
          // serialize locations list to json
          Gson gson = new GsonBuilder().create();
          String locationsJson = gson.toJson(locations);
          // post to routing engine
          RequestBody postLocationsRequestBody = RequestBody.create(JSON,locationsJson); 
          Request postLocationsRequest = new Request.Builder()
          .url(&quot;http://localhost:4567/routingEngine/sendLocations&quot;)
          .post(postLocationsRequestBody)
          .build();
         
          Call postLocationsCall = httpClient.newCall(postLocationsRequest);
          Response postLocationsResponse = postLocationsCall.execute();
      
          // get distance of default route
  
          // generate parameters
  
          Unit unit = Unit.METRIC;
  
          LocalDateTime temp = LocalDateTime.now();  
          Instant departureTime= temp.atZone(ZoneOffset.UTC)
          .withYear(2025)
          .withMonth(8)
          .withDayOfMonth(18)
          .withHour(10)
          .withMinute(12)
          .withSecond(10)
          .withNano(900)
          .toInstant(); 
          boolean optimizeWaypoints = true;
          String optimizeWaypointsString = (optimizeWaypoints == true) ? &quot;true&quot; : &quot;false&quot;;
          TrafficModel trafficModel = TrafficModel.BEST_GUESS;
          // restrictions
          RouteRestriction[] restrictions = {RouteRestriction.TOLLS,RouteRestriction.FERRIES};
          String region = &quot;us&quot;; // USA
          String language = &quot;en-EN&quot;;
          RequestParameters requestParameters = new RequestParameters(unit,departureTime,true,trafficModel,restrictions,region,language);
          
          // build url
          HttpUrl url = new HttpUrl.Builder()
          .scheme(&quot;http&quot;)
          .host(&quot;127.0.0.1&quot;)
          .port(4567)
          .addPathSegment(&quot;routingEngine&quot;)
          .addPathSegment(&quot;getDefaultRoute&quot;)
          .addPathSegment(&quot;distance&quot;)
          .build();
  
          // build request
          Request getDefaultRouteDistanceRequest = new Request.Builder()
          .url(url)
          .post(RequestBody.create(JSON,gson.toJson(requestParameters)))
          .build();
  
          // send request
          Call getDefaultRouteDistanceCall = httpClient.newCall(getDefaultRouteDistanceRequest);
          Response getDefaultRouteDistanceResponse = getDefaultRouteDistanceCall.execute();
          // store and print response
          Distance defaultRouteDistance = gson.fromJson(getDefaultRouteDistanceResponse.body().string(),Distance.class);
          System.out.println(&quot;Default Route Distance: &quot; + defaultRouteDistance);
  
      }
  }
  </code></pre>
  <p>Both classes use the following class RequestParameters to package all the request parameters together (i.e. unit, departure time, region, language etc...) just for convenience</p>
  <pre class="lang-java prettyprint-override"><code>package com.mycompany.app;
  
  import com.google.maps.model.Unit;
  import java.time.Instant;
  import com.google.maps.model.TrafficModel;
  import com.google.maps.DirectionsApi.RouteRestriction;
  
  public class RequestParameters
  {
      private Unit unit;
      private Instant departureTime;
      private boolean optimizeWaypoints;
      private TrafficModel trafficModel;
      private RouteRestriction[] restrictions;
      private String region;
      private String language;
  
      public RequestParameters(Unit unit, Instant departureTime, boolean optimizeWaypoints, TrafficModel trafficModel, RouteRestriction[] restrictions, String region, String language)
      {
          this.unit = unit;
          this.departureTime = departureTime;
          this.optimizeWaypoints = optimizeWaypoints;
          this.trafficModel = trafficModel;
          this.restrictions = restrictions;
          this.region = region;
          this.language = language;       
      }
  
      // getters
      public Unit getUnit()
      {
          return this.unit;
      }
  
      public Instant getDepartureTime()
      {
          return this.departureTime;
      }
  
      public boolean optimizeWaypoints()
      {
          return this.optimizeWaypoints;
      }
  
      public TrafficModel getTrafficModel()
      {
          return this.trafficModel;
      }
  
      public RouteRestriction[] getRestrictions()
      {
          return this.restrictions;
      }
  
      public String getRegion()
      {
          return this.region;
      }
  
      public String getLanguage()
      {
          return this.language;
      }
  
      // setters
      public void setTrafficModel(TrafficModel trafficModel)
      {
          this.trafficModel = trafficModel;       
      }
  
      public void setRegion(String region)
      {
          this.region = region;       
      }
  
      public void setLanguage(String language)
      {
          this.language = language;       
      }   
  
  }
  </code></pre>
  <p>Hopefully this provides the information necessary to investigate the problem.</p>