killbill-client-java icon indicating copy to clipboard operation
killbill-client-java copied to clipboard

Java client throws IOException for 201 responses due to missing body

Open steven-barnes opened this issue 1 year ago • 7 comments

        Account body = new Account();
        body.setName("test3");
        body.setCurrency(Currency.USD);
        body.setExternalKey("89f20977-f937-4bd7-82f9-ac4ef07b9999");
        body.setEmail("[email protected]");
        accountApi.createAccount(body, requestOptions);

This results in an IOException, as there is no response body. Running the code again results in a 409 exception, Account already exists for key 89f20977-f937-4bd7-82f9-ac4ef07b9999, indicating the account was successful created.

When creating similar accounts in the swagger page, I see the response code is 201, but the server does not return a body. The swagger page suggests that an Account object will be returned. This conflicts with the information in the docs, which says that the endpoint will return a URL in the location header.

curl -X POST "https://killbill.dev.commerce.comcast.com/1.0/kb/accounts" -H "accept: application/json" -H "X-Killbill-CreatedBy: testing" -H "authorization: Basic REDACTED" -H "X-Killbill-ApiKey: REDACTED" -H "X-Killbill-ApiSecret: REDACTED" -H "Content-Type: application/json" -d "{ \"name\": \"test99\", \"externalKey\": \"89f20977-f937-4bd7-82f9-ac4ef0999999\", \"email\": \"[email protected]\", \"currency\": \"USD\"}"

steven-barnes avatar Nov 15 '24 22:11 steven-barnes

Indeed, Kill Bill doesn't return the body upon creation (https://docs.killbill.io/latest/quick_start_with_kb_api#_step_3_create_an_account), but you can ask the client to fetch it automatically using requestOptions.withFollowLocation(true): https://github.com/killbill/killbill-client-java/blob/910bcdc58bbf6be08f4b18c5e3bb826837bdcef9/src/main/java/org/killbill/billing/client/KillBillHttpClient.java#L336

IIRC we could never really tell Swagger about this behaviour.

pierre avatar Nov 16 '24 09:11 pierre

I have tried that, and still get an IOException:

RequestOptions.builder
      .withCreatedBy("CS2")
      .withFollowLocation(Boolean.TRUE)
      .build()

Looking at the code for createAccount, it seems to be setting followLocation to TRUE by default.

steven-barnes avatar Nov 18 '24 20:11 steven-barnes

Are you following https://docs.killbill.io/latest/java_client ?

If so, could you share a Main class that reproduces the issue on a vanilla installation? The Java client is used pervasively throughout our test suite, and I don't know of any regression.

pierre avatar Nov 18 '24 20:11 pierre

I am using the latest version in Maven Central, 1.3.6. I have coded a test in Java, and still get an IOException:

package org.example;

import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.client.*;
import org.killbill.billing.client.api.gen.*;
import org.killbill.billing.client.model.gen.Account;

public class Main {

    static KillBillHttpClient client = new KillBillHttpClient(REDACTED);

    static AccountApi accountApi = new AccountApi(client);

    public static void main(String[] args) {
        var requestOptions = RequestOptions.builder()
                .withCreatedBy("CS2")
                .withFollowLocation(Boolean.TRUE)
                .build();

        var body = new Account();
        body.setName("test");
        body.setCurrency(Currency.USD);
        body.setExternalKey("xyzzy2");
        body.setEmail("[email protected]");

        try {
            var result = accountApi.createAccount(body, requestOptions);
            System.out.println(result);
        } catch (KillBillClientException e) {
            e.printStackTrace();
        }
    }
}

steven-barnes avatar Nov 18 '24 23:11 steven-barnes

Unable to reproduce.

  • Can you check what's different with the below?
  • Can you check the Kill Bill logs?

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-app</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>my-app</name>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>11</maven.compiler.release>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.kill-bill.billing</groupId>
      <artifactId>killbill-client-java</artifactId>
      <version>1.3.6</version>
    </dependency>
  </dependencies>
  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to
      parent pom) -->
      <plugins>
        <!-- clean lifecycle, see
        https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.4.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see
        https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.3.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.13.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.3.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.4.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>3.1.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>3.1.2</version>
        </plugin>
        <!-- site lifecycle, see
        https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.12.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.6.1</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>com.mycompany.app.App</mainClass>
            </manifest>
          </archive>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>single</goal>
            </goals>
            <phase>package</phase>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

App.java:

package com.mycompany.app;

import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.client.KillBillClientException;
import org.killbill.billing.client.*;
import org.killbill.billing.client.api.gen.*;
import org.killbill.billing.client.model.gen.Account;

public class App {
    public static void main(String[] args) {
        String username = "admin";
        String password = "password";
        String apiKey = "bob";
        String apiSecret = "lazar";
        String serverHost = "localhost";
        int serverPort = 8080;
        String kbServerUrl = String.format("http://%s:%d", serverHost, serverPort);
        KillBillHttpClient killBillHttpClient = new KillBillHttpClient(kbServerUrl, username, password, apiKey,
                apiSecret);
        AccountApi accountApi = new AccountApi(killBillHttpClient);

        var requestOptions = RequestOptions.builder()
                .withCreatedBy("CS2")
                .withFollowLocation(Boolean.TRUE)
                .build();

        var body = new Account();
        body.setName("test");
        body.setCurrency(Currency.USD);
        body.setExternalKey("xyzzy");
        body.setEmail("[email protected]");

        try {
            var result = accountApi.createAccount(body, requestOptions);
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Execution:

$ java -jar target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar
SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
class Account {
    org.killbill.billing.client.model.gen.Account@fc5e4c1c
    accountId: 5dbaf858-5447-4262-8418-ccae0e26ca31
    name: test
    firstNameLength: null
    externalKey: xyzzy
    email: [email protected]
    billCycleDayLocal: 0
    currency: USD
    parentAccountId: null
    isPaymentDelegatedToParent: false
    paymentMethodId: null
    referenceTime: 2024-11-19T08:14:40.000Z
    timeZone: UTC
    address1: null
    address2: null
    postalCode: null
    company: null
    city: null
    state: null
    country: null
    locale: null
    phone: null
    notes: null
    isMigrated: null
    accountBalance: null
    accountCBA: null
    auditLogs: []
}
$ java -jar target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar
SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
org.killbill.billing.client.KillBillClientException: Account already exists for key xyzzy
        at org.killbill.billing.client.KillBillHttpClient.throwExceptionOnResponseError(KillBillHttpClient.java:390)
        at org.killbill.billing.client.KillBillHttpClient.doPrepareRequestInternal(KillBillHttpClient.java:349)
        at org.killbill.billing.client.KillBillHttpClient.doPrepareRequest(KillBillHttpClient.java:296)
        at org.killbill.billing.client.KillBillHttpClient.doPost(KillBillHttpClient.java:210)
        at org.killbill.billing.client.KillBillHttpClient.doPost(KillBillHttpClient.java:205)
        at org.killbill.billing.client.api.gen.AccountApi.createAccount(AccountApi.java:176)
        at com.mycompany.app.App.main(App.java:34)

Java version:

$ java -version
openjdk version "11.0.14.1" 2022-02-08 LTS
OpenJDK Runtime Environment Microsoft-31205 (build 11.0.14.1+1-LTS)
OpenJDK 64-Bit Server VM Microsoft-31205 (build 11.0.14.1+1-LTS, mixed mode)

pierre avatar Nov 19 '24 08:11 pierre

Using the debugger, I can see that the location URL is using http, the request to killbill is using https:

    static KillBillHttpClient client = new KillBillHttpClient(
            "https://killbill.dev.commerce.comcast.com",

http://killbill.dev.commerce.comcast.com:80/1.0/kb/accounts/16c15fcb-f461-4ae0-baff-ec114883496a

We are self-hosting KB in AWS, and I am testing from my laptop.

steven-barnes avatar Nov 19 '24 18:11 steven-barnes

Is the Location header returning HTTP instead of HTTPS? What is terminating SSL, Tomcat or an intermediate load balancer? If the latter, you might need to configure the load balancer to send x-forwarded-proto.

Related:

  • https://github.com/killbill/killbill/issues/566
  • https://groups.google.com/g/killbilling-users/c/Ofs6J0oGZpI

pierre avatar Nov 20 '24 09:11 pierre