Skip to content

Merge the Smithy server into org.restlet.ext.smithy, on par with its client - #1674

Open
jlouvel wants to merge 4 commits into
3.0from
feat/smithy-server-in-ext
Open

jlouvel wants to merge 4 commits into
3.0from
feat/smithy-server-in-ext

Conversation

@jlouvel

@jlouvel jlouvel commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1673. The Smithy server of #1638 moves into org.restlet.ext.smithy, beside the client, and catches up with it.

One artifact

SmithyServerAdapter and SmithyServerHost move into org.restlet.ext.smithy, and the org.restlet.ext.smithy.server artifact goes away. It has never been released, so changes.md rewrites its Milestone 2 entry rather than recording a breaking change.

smithy-java 1.8.0's server-api and server-core still split package software.amazon.smithy.java.server (smithy-java#1437), so the module can't require them:

  • server-core is an optional dependency, so client-only users don't get it. Maven puts it on the class path, outside the module graph.
  • The module compiles with --add-reads org.restlet.ext.smithy=ALL-UNNAMED. @SuppressWarnings("exports") covers the SDK types that the public API exposes.
  • Named consumers of the server pass the same --add-reads, at compile time and at run time. SmithyModuleTestCase.namedConsumerHostsAService covers this case.

Decision to check in review: whether this class-path-reading setup is acceptable in a named module until #1437 ships. The alternative is to keep a separate artifact without a descriptor.

Parity with the client

Client Server, before Server, now
SDK discovery ClientTransportFactory restlet none ServerProvider restlet: by name, or when Netty is absent (priority -1, Netty 0)
Explicit API SmithyClientAdapter SmithyServerAdapter SmithyServerAdapter, and a new public SmithyServerBuilder
Protocols tested restJson1, RPCv2 CBOR restJson1 restJson1, RPCv2 CBOR, over HTTP/1.1 and HTTP/2
Named modules tested class path only tested, with --add-reads

SmithyServerBuilder implements the SDK's ServerBuilder, with one Restlet server connector per endpoint (http or https) and one adapter per route:

  • numberOfWorkers sets each route's concurrent-call limit.
  • context(Context) passes connector parameters (HTTP/2, TLS), and maxRequestBytes and maxCallDuration set the adapters' limits.
  • A call goes to the first route whose path prefix (decoded), host name, and connector port and scheme match it, and whose services resolve its operation; otherwise it falls through to the next route, as attachTo scores routes. The port and scheme come from the connector that took the call, not from the Host header.
  • When routes only set path prefixes, the longest matching prefix wins: the SDK's builder keeps addService(path, …) mappings in a HashMap, so a / route could otherwise take calls meant for a longer prefix. Routes that also match hosts, ports or schemes keep the order they were added in.
  • The SDK's ServiceMatcher is not used for this. It ignores host names, and compares an unset port (null) with -1.
  • Endpoints need a host, and may only have a scheme, a host and a port. IPv6 hosts lose their brackets before reaching the connector.
  • A failed start keeps its cause when stopping what started fails too.
  • Without server-core, the discovered provider fails with a clear error rather than a NoClassDefFoundError at build time.
  • The provider has priority -1 so that adding this extension for its client doesn't replace an application's Netty server.

The provider is declared only in META-INF/services. A provides clause would need the split package, so Server.builder() discovers the builder on the class path only.

Upstream findings (smithy-java 1.8.0, unchanged on main)

  • The RPCv2 server serializes a modeled error without its __type, so clients get a CallException rather than the modeled error. cborModeledErrorsLoseTheirTypeUpstream pins it.
  • Over RPCv2 CBOR, missing @required members aren't rejected: the input comes with zero values. Range and length constraints are still enforced, and invalidInputIsRejected tests one.

Tests (70, coverage met, no SpotBugs findings)

  • SmithyServerBuilderTestCase (new, 17): fallthrough to a route resolving the call, decoded prefixes, IPv6 addresses, a CBOR round trip over HTTP/1.1 and HTTP/2, validation, the upstream error bug, prefix, port, scheme and host routing, a Host header naming another port, the longest prefix winning, host routes keeping their order, limits, invalid settings, single use, a failed start stopping what it started, and the provider.
  • SmithyModuleTestCase: the server jars are now kept off the module path, and two child-JVM consumers are added:
    • a named module hosting the generated restJson1 Notes service;
    • a class-path consumer whose server comes from Server.builder() discovery and whose CBOR client uses the discovered transport.
  • SmithyServerAdapterTestCase moved unchanged, apart from the generated package.
  • Test code generation now uses one smithy-build projection per model: greeter, notes and the new counter.

🤖 Generated with Claude Code

jlouvel and others added 4 commits November 14, 2026 10:32
…ient

The server moves from the org.restlet.ext.smithy.server artifact into
org.restlet.ext.smithy, which reads the SDK server, an optional
dependency, on the class path: smithy-java 1.8.0 splits its package.
A ServerProvider named restlet and a public SmithyServerBuilder let
Server.builder() discover Restlet connectors, as SDK clients discover
the transport. RPCv2 CBOR, a named consumer hosting a service and
class-path discovery are now tested.

Closes #1673

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Routes match the port and scheme of the connector that took a call,
not those of its Host header. Path prefixes are only reordered when
routes set nothing else, so that routes on hosts, ports or schemes
keep their order. Endpoints with a path, query, fragment or user info,
routes without services and null durations are rejected. The server
provider now comes after the SDK's Netty server, so that adding the
extension for its client doesn't change an application's server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A call now goes to the first matching route whose services resolve its
operation, so a longer prefix or an earlier route no longer hides the
operations of later ones. Prefixes match the decoded path, endpoints
need a host, IPv6 hosts lose their brackets, a failed start keeps its
cause, and the provider reports a missing server-core clearly. The
server class moves next to the adapter, holding each route with its
adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like the compiler, the Javadoc tool needs --add-reads
org.restlet.ext.smithy=ALL-UNNAMED to resolve the smithy-java server
types, which stay outside the module graph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant