Wednesday, March 15, 2023

Patching PATCH for JDK 11


Last post I talked about HttpsURLConnection, and how one could intercept calls to the underlying socket to report on the HTTP protocol messages.  In this post, I'll explain how you can work around another shortcoming in Java where HttpURLConnection (and thus HttpsURLConnection which derives from it) fail to support the HTTP PATCH method defined in RFC5789.

The critical problem comes from the fact that this doesn't work:

  URL url = new URL("https://localhost");
  HttpURLConnection con = (HttpURLConnection)url.openConnection();
  con.setRequestMethod("PATCH");

It will throw a ProtocolException.  Here's the JDK Source code for the static variables and method in question:

  /* valid HTTP methods */
  private static final String[] methods = {
    "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
  };

  /**
    * Set the method for the URL request, one of:
    * <UL>
    * <LI>GET
    * <LI>POST
    * <LI>HEAD
    * <LI>OPTIONS
    * <LI>PUT
    * <LI>DELETE
    * <LI>TRACE
    * </UL> are legal, subject to protocol restrictions. The default
    * method is GET.
    * @param method the HTTP method
    * @exception ProtocolException if the method cannot be reset or if
    * the requested method isn't valid for HTTP.
    * @exception SecurityException if a security manager is set and the
    * method is "TRACE", but the "allowHttpTrace"
    * NetPermission is not granted.
    * @see #getRequestMethod()
  */

  public void setRequestMethod(String method) throws ProtocolException {
    if (connected) {
      throw new ProtocolException("Can't reset method: already connected");
    }
    // This restriction will prevent people from using this class to
    // experiment w/ new HTTP methods using java. But it should
    // be placed for security - the request String could be
    // arbitrarily long.
    for (int i = 0; i < methods.length; i++) {
      if (methods[i].equals(method)) {
        if (method.equals("TRACE")) {
          SecurityManager s = System.getSecurityManager();
          if (s != null) {
            s.checkPermission(new NetPermission("allowHttpTrace"));
          }
        }
        this.method = method;
        return;
      }
    }
    throw new ProtocolException("Invalid HTTP method: " + method);
}

What's a developer to do?  Well, you could use an alternate implementation to connect instead of HttpsURLConnection.  Some alternatives include the Apache HTTP Client, and Spring's RestTemplate.

Sometimes using an alternative isn't a choice (for example, when an underlying library uses it and you can't change the implementation).  In my particular case, I was experimenting with the tus-java-client to talk to a TUS Server to experiment with sending data to a DEX endpoint.  

The Java tus client uses HttpURLConnection, and some servers may not like the X-HTTP-Method-Override header that has been included to work around this problem with the expected use of PATCH.

A bit of Googling turns up a number of different solutions, one of which I implemented in a static method called during application startup.

private static void patchThePatch() {
  try {
    String[] methods = { "PATCH" };
    Field methodsField = HttpURLConnection.class.getDeclaredField("methods");
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(methodsField, methodsField.getModifiers() & ~Modifier.FINAL);
    methodsField.setAccessible(true);
    String[] oldMethods = (String[]) methodsField.get(null);
    Set<String> methodsSet = new LinkedHashSet<>(Arrays.asList(oldMethods));
    methodsSet.addAll(Arrays.asList(methods));
    String[] newMethods = methodsSet.toArray(new String[0]);
    methodsField.set(null/*static field*/, newMethods);
    URL url = new URL("https://localhost");
    HttpURLConnection con = (HttpURLConnection)url.openConnection();
    con.setRequestMethod("PATCH");
    // Turn off caching
    con.setDefaultUseCaches(false);
    LOGGER.info("PATCH updated in HttpURLConnection");
  } catch (Exception ex) {
    LOGGER.info("Could not adjust HttpURLConnection to accept PATCH: {}", ex.getMessage(), ex);
  }
}

This code was inspired by:

https://stackoverflow.com/questions/25163131/httpurlconnection-invalid-http-method-patch/40606633#40606633

If you are using a JDK later than JDK-11, you might want to look into:

https://stackoverflow.com/questions/56039341/get-declared-fields-of-java-lang-reflect-fields-in-jdk12



0 comments:

Post a Comment