Skip to content

Generate sealed interfaces to match against endpoint associated errors#2696

Merged
bulldozer-bot[bot] merged 9 commits intodevelopfrom
mpritham/endpoint-assoc-error-utility
Nov 5, 2025
Merged

Generate sealed interfaces to match against endpoint associated errors#2696
bulldozer-bot[bot] merged 9 commits intodevelopfrom
mpritham/endpoint-assoc-error-utility

Conversation

@mpritham
Copy link
Contributor

@mpritham mpritham commented Oct 27, 2025

Before this PR

In #2401 we added support for defining endpoint associated errors. This allowed Conjure service owners to define the errors that clients should expect to handle. This work was extended in #2435 where Dialogue client interfaces were generated to return sealed interfaces permitting records that corresponded to each endpoint error, and a "success" type, acting as a "result" type.

Initially the stance was that endpoint errors defined by services need to be handled by clients: using result types (or checked exceptions) would nudge callers of a client's endpoint to handle all of the potential errors a service has associated with the endpoint. (This could be worked around by utility methods that extract specific "success" types out of the provided result type but that would be an intentional choice made by clients.) In practice, this is a restrictive stance that hurts adoption: client code does not always need to handle the errors that endpoints have associated but if they would like to, they should have a way to know what errors should be handled.

Another reason to not change the return type of all endpoints in all Dialogue client interfaces is that it could lead to ABI breaks.

After this PR

For each endpoint that has associated errors in its definition, there will be a new sealed interface generated that allows devs to convert a generic RemoteException into one of the error-specific exceptions. The sealed interface allows devs on Java 21+ to use exhaustive pattern matching switch statements providing compile-time guarantees that they have handled all possible endpoint associated errors.

For example, consider this endpoint definition:

ErrorService:
  name: Error Service
  package: com.palantir.product
  default-auth: header
  base-path: /errors
  endpoints:
    testBasicError:
      http: POST /basic
      args:
        shouldThrowError: boolean
      returns: string
      errors:
        - InvalidArgument
        - ConflictingCauseSafeArgErr

The following Dialogue client interface will be generated:

public interface ErrorServiceBlocking {
    /** @apiNote {@code POST /errors/basic} */
    @ClientEndpoint(method = "POST", path = "/errors/basic")
    String testBasicError(AuthHeader authHeader, boolean shouldThrowError);
    
    // <various other methods not relevant to this feature>
    
    sealed interface TestBasicErrorErrors {
        static TestBasicErrorErrors from(RemoteException e) {
            if (e instanceof TestErrors.InvalidArgumentException ex) {
                return new InvalidArgument(ex);
            } else if (e instanceof ConjureErrors.ConflictingCauseSafeArgErrException ex) {
                return new ConflictingCauseSafeArgErr(ex);
            } else {
                return new Unknown(e);
            }
        }

        record InvalidArgument(TestErrors.InvalidArgumentException e) implements TestBasicErrorErrors {}

        record ConflictingCauseSafeArgErr(ConjureErrors.ConflictingCauseSafeArgErrException e)
                implements TestBasicErrorErrors {}

        record Unknown(RemoteException e) implements TestBasicErrorErrors {}
    }
}

This allows clients to catch RemoteExceptions thrown by the call to testBasicError and convert it into a TestBasicErrorErrors object.

==COMMIT_MSG==
Generate sealed interfaces to match against endpoint associated errors
==COMMIT_MSG==

Possible downsides?

@changelog-app
Copy link

changelog-app bot commented Oct 27, 2025

Generate changelog in changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

Generate sealed interfaces to match against endpoint associated errors

Check the box to generate changelog(s)

  • Generate changelog entry

record Unknown(RemoteException e) implements TestBasicErrorErrors {}
}

sealed interface TestImportedErrorErrors {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably nicer for the suffix to be singular.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For reviewers: this file (or the async interface) contains the relevant newly generated code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Singular definitely makes sense since the actual instance will be a single type. Also thoughts on ErrorType or something for the suffix? Was also thinking AssociatedError, but that feels like a mouthful

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hm, what are your thoughts on just keeping the name <EndpointName>Error? Feel like it's cleaner.

Copy link
Contributor

Choose a reason for hiding this comment

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

sure works for me

@mpritham mpritham force-pushed the mpritham/endpoint-assoc-error-utility branch from 8c13809 to 65ee1b8 Compare October 27, 2025 22:27
ErrorGenerationUtils.errorTypesClassName(error.getError().getNamespace()),
ErrorGenerationUtils.errorExceptionClassName(errorName));
TypeSpec errorRecord = createRecordForEndpointErrorUtility(errorName, exceptionClassName, utiltyClassName);
// Not strictly required. Seems a bit cleaner to not have it.
Copy link
Contributor Author

@mpritham mpritham Oct 27, 2025

Choose a reason for hiding this comment

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

This is because of JLS-9.1.4. I'll remove the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suspect these sealed interfaces are small so it might be quite easy to just see all of the sub-interfaces. For things like the generated union classes (which were much larger), I think it's nice to have the list of permitted interfaces in one line at a glance. I can be convinced that these sealed interfaces could be large for endpoints with loads of associated errors and we should add them. Not super opinionated here.


record EndpointError(EndpointSpecificErrors.EndpointErrorException e) implements TestImportedErrorErrors {}

record Unknown(RemoteException e) implements TestImportedErrorErrors {}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially did not have this Unknown type, and just threw an exception if the RemoteException wasn't an endpoint associated error, but I think this will lead to nested try-catch blocks in code. Devs will likely want to handle the "unknown" case anyway so creating a separate record for it that they can match on makes sense.

} catch (RemoteException e) {
TestBasicErrorErrors error = ErrorServiceBlocking.TestBasicErrorErrors.from(e);
assertThat(error).isInstanceOfSatisfying(TestBasicErrorErrors.InvalidArgument.class, invalidArgument -> {
assertThat(invalidArgument.e().error().parameters().field()).isEqualTo("field");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can change this to .exception.

record Unknown(RemoteException e) implements TestBasicErrorErrors {}
}

sealed interface TestImportedErrorErrors {
Copy link
Contributor

Choose a reason for hiding this comment

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

Singular definitely makes sense since the actual instance will be a single type. Also thoughts on ErrorType or something for the suffix? Was also thinking AssociatedError, but that feels like a mouthful

Comment on lines 271 to 274
if (e instanceof TestErrors.InvalidArgumentException ex) {
return new InvalidArgument(ex);
} else if (e instanceof ConjureErrors.ConflictingCauseSafeArgErrException ex) {
return new ConflictingCauseSafeArgErr(ex);
Copy link
Contributor

Choose a reason for hiding this comment

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

Aren't the series of instanceof checks unideal from a performance standpoint? If we were on source 21+ I would say a switch statement would be an easy replacement over a visitor

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have an option without instanceof in the absence of source 21+? Visitor patterns with overloads would have to rely on the RemoteException having an accept method to take a visitor, which simply won't work here 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think we can actually match on the error name here. I can make that change.

}
}

record InvalidArgument(TestErrors.InvalidArgumentException e) implements TestBasicErrorErrors {}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we should name the parameter error or something so that the getter method is not .e()

Comment on lines 208 to 213
ClassName utiltyClassName = ClassName.get(
packageName,
className.simpleName(),
CaseFormat.LOWER_CAMEL.to(
CaseFormat.UPPER_CAMEL,
endpointDef.getEndpointName().get()) + "Errors");
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you could use ClassName#nestedClass here

Copy link
Contributor

@aldexis aldexis left a comment

Choose a reason for hiding this comment

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

Didn't look through the codegen pieces, but at the generated code. The new helpers seem like a good addition and shouldn't break anything.

The only ABI risks I could find are 1. having it adopted and reverting usage of the flag 2. Removing associations between errors and endpoints (even with the error still existing) I think this is probably fine.

I'm not entirely sure the abi checker would protect us from the removal in all cases however, specifically if the usage looks like

switch(TestImportedErrorErrors.from(exception)) {
  EndpointError -> log.error("Found an endpoint error");
  Unknown -> log.error("Found an unknown error");
}

where the class itself isn't used anywhere

Beyond this, I agree with Kunal's comments and will leave the deeper review to him

kunalrkak
kunalrkak previously approved these changes Nov 3, 2025
Copy link
Contributor

@kunalrkak kunalrkak left a comment

Choose a reason for hiding this comment

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

Thanks!

@policy-bot policy-bot bot dismissed kunalrkak’s stale review November 4, 2025 16:27

Invalidated by push of 0e40e01

@changelog-app
Copy link

changelog-app bot commented Nov 4, 2025

Successfully generated changelog entry!

Need to regenerate?

Simply interact with the changelog bot comment again to regenerate these entries.


📋Changelog Preview

💡 Improvements

  • Generate sealed interfaces to match against endpoint associated errors (#2696)

@bulldozer-bot bulldozer-bot bot merged commit f6afecb into develop Nov 5, 2025
5 checks passed
@bulldozer-bot bulldozer-bot bot deleted the mpritham/endpoint-assoc-error-utility branch November 5, 2025 20:20
@autorelease3
Copy link

autorelease3 bot commented Nov 5, 2025

Released 8.61.0

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.

3 participants