javaspring-bootrestlinkedin-api

PUT request working using postman but not using java, spring boot


I am trying to make an API call in my Spring Boot project. The PUT request is working fine using postman and I am getting 204 status code(no-content). Whereas the JAVA code is throwing 500 internal server error. The URL generated from code (as per logs) is similar to the one in postman. Here is the partial code:

private void subscribe() {            
    String url = "https://api.linkedin.com/rest/eventSubscriptions/(developerApplication:urn%3Ali%3AdeveloperApplication%3A{dev_app_id},user:urn%3Ali%3Aperson%3A{person_id},entity:urn%3Ali%3Aorganization%3A{organization_id},eventType:ORGANIZATION_SOCIAL_ACTION_NOTIFICATIONS)";
    url.replace("{dev_app_id}", DEVELOPER_APPLICATION_ID);
    url.replace("{person_id}", "iTFpJkMpIl");
    url.replace("{organization_id}", ORGANIZATION_ID);

    // headers
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.add("X-Restli-Protocol-Version", "2.0.0");
    httpHeaders.add("LinkedIn-Version", VERSION);
    httpHeaders.setContentType(MediaType.APPLICATION_JSON);
    httpHeaders.set(HttpHeaders.AUTHORIZATION, new StringBuilder("Bearer ").append(channel.getAccessToken()).toString());

    //body
    Webhook body = new Webhook(callback_url);
    HttpEntity<Webhook> entity = new HttpEntity<Webhook>(body, httpHeaders);
    logger.info("Hitting subscription API: {} with body: {}", url, getJson(entity.getBody()));
    logger.info("Headers: {}", entity.getHeaders().toSingleValueMap());
    try {
        restTemplate.put(url, entity);
    } catch (RestClientException e) {
        e.printStackTrace();
    }
}

// just to check logs
private String getJson(Webhook body) {
    try {
        return new ObjectMapper().writeValueAsString(body);
    } catch (Exception e) {
        logger.error("Error converting object to JSON.", e);
        return "";
    }
}

The Webhook class:

public class Webhook {
    private String webhook;

    public Webhook(String webhook) {
        this.webhook = webhook;
    }

    public String getWebhook() {
        return webhook;
    }

    public void setWebhook(String webhook) {
        this.webhook = webhook;
    }
}

The documentation here states the URL required to be generated. The specific URNs are encoded.

The error:

org.springframework.web.client.HttpServerErrorException$InternalServerError: 500 Server Error: "{"message":"Internal Server Error","status":500}"
        at org.springframework.web.client.HttpServerErrorException.create(HttpServerErrorException.java:102)
        at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:186)
        at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:137)
        at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
        at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:942)
        at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:891)
        at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:790)
        at org.springframework.web.client.RestTemplate.put(RestTemplate.java:567)
        at com.germin8.main.services.EventSubscriber.lambda$0(EventSubscriber.java:152)
        at java.base/java.util.AbstractList$RandomAccessSpliterator.forEachRemaining(AbstractList.java:720)
        at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
        at com.germin8.main.services.EventSubscriber.createSubscriptions(EventSubscriber.java:131)
        at com.germin8.main.services.EventSubscriber.organizarionSubscriptionMonitor(EventSubscriber.java:99)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130)
        at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124)
        at io.micrometer.observation.Observation.observe(Observation.java:499)
        at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124)
        at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
        at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
        at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:305)
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.base/java.lang.Thread.run(Thread.java:840)

Here is my pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/>
    </parent>
    <groupId>com.demo</groupId>
    <artifactId>linkedin-crawler</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <name>linkedin-crawler</name>
    <description>Linkedin Crawling Service</description>
    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>32.1.3-jre</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>1_maven.apache.org</id>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
            <url>https://repo.maven.apache.org/maven2</url>
            <layout>default</layout>
        </repository>
        <repository>
          <id>jcenter-snapshots</id>
          <name>jcenter</name>
          <url>https://jcenter.bintray.com/</url>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

I have tried

  1. hitting the endpoint without encoding.
  2. Using exchange instead of put.
restTemplate.exchange(url, HttpMethod.PUT, entity, String.class);

To check if the body is serialising correctly, I made a similar request to a local endpoint and it was correct too.
To check the headers, I had logged them and compared them to the ones in my postman request.


Solution

  • It turns out that on passing a manually encoded url, it got encoded again, which led to double encoding.
    Whereas, if encoding is not performed, the PUT request doesn't encode it at all. Don't know the exact reason of why this is the case, but here is the solution by disabling the default encoding.

    DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
    factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
    restTemplate = new RestTemplateBuilder()
                      .uriTemplateHandler(factory)
                      .defaultHeader("X-Restli-Protocol-Version", "2.0.0")
                      .defaultHeader("LinkedIn-Version", VERSION)
                      .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                      .build();