Skip to content

Add support for idempotency keys when creating tickets#808

Merged
PierreBtz merged 16 commits intocloudbees-oss:masterfrom
aleksei-averchenko-wise:create-ticket-idempotency-key
Apr 15, 2026
Merged

Add support for idempotency keys when creating tickets#808
PierreBtz merged 16 commits intocloudbees-oss:masterfrom
aleksei-averchenko-wise:create-ticket-idempotency-key

Conversation

@aleksei-averchenko-wise
Copy link
Copy Markdown
Contributor

@aleksei-averchenko-wise aleksei-averchenko-wise commented Mar 19, 2026

This change implements support for Zendesk's idempotency key
feature
when creating tickets. I happen to need to use this feature myself :)

I added two new API methods: createTicketIdempotent and createTicketIdempotentAsync. See the usage example in the updated README.md.

Copy link
Copy Markdown
Collaborator

@PierreBtz PierreBtz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! Thanks for the contribution. I did not have time yet to look into it in details, but I wonder if we could maybe improve the architecture.

What you suggest is to add the idempotency fields as part of the Ticket model, which seems to mix two different concerns:

  • the ticket model itself
  • the idempotency feature, which is not really part of the Ticket model but a feature of the ticket creation flow

As a consequence of this choice, it means that stored tickets/serialized ticket will keep those fields which have no real value after the ticket creation flow is done (if I have a null what does that mean exactly?).

A potential approach to this problem would be to avoid changing the Ticket model (keep it aligned with the ZD one) and create a TicketResult or even a ZendeskResult<T> class if we want to be future proof that would contain both the model and the additional feature of the creation flow, so roughtly:

  public class CreateResult<T> {

    private final T entity;
    private final String idempotencyKey;
    private final boolean idempotencyHit;

// getters setters...
// we really need to bump from java 11 to have records...
}

Then you would keep the current createTicket methods and create new methods:

  public CreateResult<Ticket> createTicketIdempotent(Ticket ticket, String idempotencyKey) {
    return complete(createTicketIdempotentAsync(ticket, idempotencyKey));
  }

  public ListenableFuture<CreateResult<Ticket>> createTicketIdempotentAsync(
      Ticket ticket, String idempotencyKey) {
    // create the request
    // header addition
    // completion handler specific to idempotent feature
    // submit
  }

and so on...

Call for end user would look like:

  CreateResult<Ticket> result = zendesk.createTicketIdempotent(ticket, "key-123");
  Ticket created = result.getEntity();
  if (result.isIdempotencyHit()) { 
  // do stuff 
}

@aleksei-averchenko-wise
Copy link
Copy Markdown
Contributor Author

A potential approach to this problem would be to avoid changing the Ticket model (keep it aligned with the ZD one) and create a TicketResult or even a ZendeskResult<T> class if we want to be future proof that would contain both the model and the additional feature of the creation flow

Sounds good! I thought of that initially but then decided to be clever and try and sneak the feature into existing calls to make adoption easier on my end. I'll redo it the way you suggested.

@aleksei-averchenko-wise
Copy link
Copy Markdown
Contributor Author

aleksei-averchenko-wise commented Apr 8, 2026

@PierreBtz a couple of notes:

  • I added test cases in RealSmokeTest, but I don't have access to your sandbox account. Can you run it to verify that the tests still pass, please?
  • In Javadocs, I added @since 1.5.0, because that's what I assume the new release is going to be called. I haven't bumped the version in pom.xml though because I saw in git blame that you tend to do it separately. Let me know if I should change anything in this regard.
  • I took the liberty of adding AGENTS.md because Claude Code kept stumbling when trying to run Maven with my system default version of Java. I can remove it if you don't like it :)

I tried to follow the contribution guidelines to the extent that the existing code follows them, hope that's good enough.

Thanks!

@aleksei-averchenko-wise
Copy link
Copy Markdown
Contributor Author

I updated README.md several times to make sure that the usage example is as simple as it can be. Should be alright now.

@aleksei-averchenko-wise
Copy link
Copy Markdown
Contributor Author

I'm truly finished with README.md now. Ideally we should have someone from Zendesk take a look at it for sanity checking, but I don't think it's strictly necessary.

Copy link
Copy Markdown
Collaborator

@PierreBtz PierreBtz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't have time to completely review this this week. I'll do it next week.

I much prefer this iteration over the last one, thanks for doing the changes.

A couple of on going comments already though:

For now I'll trigger the CI to see if the new tests pass against our sandbox.

Comment thread AGENTS.md Outdated
Comment thread AGENTS.md Outdated
@aleksei-averchenko-wise
Copy link
Copy Markdown
Contributor Author

aleksei-averchenko-wise commented Apr 13, 2026

@PierreBtz I think the Java 8 stuff in AGENTS.md is correct, I've just tested that it indeed does what Claude said. It refers to this section of pom.xml:

                <checkSignatureRule implementation="org.codehaus.mojo.animal_sniffer.enforcer.CheckSignatureRule">
                  <signature>
                    <groupId>org.codehaus.mojo.signature</groupId>
                    <artifactId>java18</artifactId>
                    <version>1.0</version>
                  </signature>
                </checkSignatureRule>

I think what it does is it tries to validate the compiled bytecode against various JVM versions, as opposed to validating the Java source code itself. So even though we use lambdas left and right, it's okay because they get compiled away.

I checked that java18 really does mean Java 8 by inserting code that uses a Java 9 feature:

    VarHandle longArrayViewHandle = MethodHandles.byteArrayViewVarHandle(
        long[].class, java.nio.ByteOrder.BIG_ENDIAN);

(see https://en.wikipedia.org/wiki/Java_version_history#Java_SE_9 and https://openjdk.org/jeps/193). The compilation (JAVA_HOME=$(/usr/libexec/java_home -v 11) mvn verify) did fail as expected:

[INFO] --- enforcer:3.6.2:enforce (check-java-compat) @ zendesk-java-client ---
[WARNING] ruleName checkSignatureRule with implementation org.codehaus.mojo.animal_sniffer.enforcer.CheckSignatureRuleuses the deprecated Maven Enforcer Plugin API. This will not be supported in a future version of the plugin. Please contact the rule maintainer to upgrade the rule implementation to the current API.
[INFO] Checking unresolved references to org.codehaus.mojo.signature:java18:1.0
[ERROR] /Users/aleksei.averchenko/dev/zendesk-java-client/src/main/java/org/zendesk/client/v2/IdempotencyUtil.java:74: Undefined reference: java.lang.invoke.VarHandle
[ERROR] /Users/aleksei.averchenko/dev/zendesk-java-client/src/main/java/org/zendesk/client/v2/IdempotencyUtil.java:74: Undefined reference: java.lang.invoke.VarHandle java.lang.invoke.MethodHandles.byteArrayViewVarHandle(Class, java.nio.ByteOrder)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.431 s
[INFO] Finished at: 2026-04-13T11:00:22+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.2:enforce (check-java-compat) on project zendesk-java-client: 
[ERROR] Rule 0: org.codehaus.mojo.animal_sniffer.enforcer.CheckSignatureRule failed with message:
[ERROR] Signature errors found. Verify them and ignore them with the proper annotation if needed.
[ERROR] -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

BTW I don't think there are any newer signature versions in the Maven repository (https://mvnrepository.com/artifact/org.codehaus.mojo.signature), and there is a deprectation warning in the log above. So it might be worth removing this rule if JVM 8 bytecode compatibility isn't intended.

@aleksei-averchenko-wise
Copy link
Copy Markdown
Contributor Author

aleksei-averchenko-wise commented Apr 13, 2026

As for catching idempotency errors, my thinking was that isIdempotencyConflict already has very tight filtering so it can't misfire, so it's safe to include this check by default. Potential alternatives could be:

  1. Keep the existing code as is - but then the exception handling logic would become very fragmented and difficult to maintain.
  2. Introduce another flag to checkStatusCode - quite feasible, but forwarding it would add to the boilerplate, so I'm not sure that it's a good idea.
  3. Somehow move this check entirely to IdempotencyUtil - I'd love that, but unfortunately from what I can tell, there isn't an easy way to do that. onThrowable should probably not be used for this purpose: any exception that it throws is propagated as is instead of being wrapped in aCompletedFuture:
// org.asynchttpclient.DefaultAsyncHttpClient
private <T> ListenableFuture<T> execute(Request request, final AsyncHandler<T> asyncHandler) {
  try {
    return requestSender.sendRequest(request, asyncHandler, null);
  } catch (Exception e) {
    asyncHandler.onThrowable(e);
    return new ListenableFuture.CompletedFailure<>(e);
  }
}

throw new ZendeskResponseRateLimitException(response);
}

if (isIdempotencyConflict(response)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two problems with this change:

  1. (minor): the json response is parsed for any non 2xx, 404 or 429 response. It adds some overhead. I think it's ok we can live with it, but still it's worth mentioning.
  2. (more problematic): if there is a parsing error for some reason, with the new code we will throw a ZendeskResponseException (the actual JsonProcessingException being the cause) while the previous code would have thrown a JsonProcessingException. This is a breaking change.

I see two options:
a. we accept the breaking change and document it in the release notes.
b. we add a flag in this method (like you already did with notFoundMeansNull to run the idempotency check) when relevant.

I would be leaning toward a, the new behavior makes sense, after all a wrong json makes sense as a ZendeskResponseException, folks can always unwrap if they want to know the cause. Thoughts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. It will only attempt parsing when response.getStatusCode() != 400, so it doesn't seem like a huge issue to me since presumably we shouldn't be getting this error code for other reasons.
  2. If I understand correctly, the code was previously never attempting to parse error response JSON, so the behaviour should be almost identical to how it was before. As an option, for extra clarity I can just swallow the exception in isIdempotencyConflict so that ZendeskResponseException is thrown exactly the way it was prior to this change by the last statement of checkStatusCode, with no suppressed exceptions added.

But like I said above, I'm open to adding another flag, it's that I would personally prefer to keep error handling as simple and unified as it can be.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost identical to how it was before

Indeed, there is a slight change (due to the fact that the handler now ends up parsing the json on non 2xx response, hence changing the exact exception error but not the exception type), my bad. I would not qualify this as breaking, if someone relies on the exact exception message in their logic, then I'm interested to see the bug report :)

Comment thread src/main/java/org/zendesk/client/v2/ZendeskResponseException.java
throw new ZendeskResponseRateLimitException(response);
}

if (isIdempotencyConflict(response)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost identical to how it was before

Indeed, there is a slight change (due to the fact that the handler now ends up parsing the json on non 2xx response, hence changing the exact exception error but not the exception type), my bad. I would not qualify this as breaking, if someone relies on the exact exception message in their logic, then I'm interested to see the bug report :)

Copy link
Copy Markdown
Collaborator

@PierreBtz PierreBtz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution

@PierreBtz PierreBtz merged commit ff34c0c into cloudbees-oss:master Apr 15, 2026
3 checks passed
@aleksei-averchenko-wise
Copy link
Copy Markdown
Contributor Author

My pleasure!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants