HTTP ports
port:
ExternalApiService:
meta: http @url("https://api.example.com") @auth("Bearer token")
methods:
getUser:
meta: method @get("/api/users/{userId}")
params:
userId: long
return: UserDTO
createUser:
meta: method @post("/api/users")
params:
body: UserRequest @body
return: UserResponse
updateUser:
meta: method @HttpEndpoint(url="/api/users/{id}", method="PUT")
params:
id: long
body: UserRequest @body
return: UserResponse
| Level | Annotation | Means |
|---|---|---|
| port | @url("baseUrl") | the server's base URL |
| port | @auth("token") | the default Authorization header |
| port | @timeout(connect=.., read=.., write=.., response=..) | timeouts in ms (defaults 10000 / 90000 / 90000 / 60000) |
| method | @get, @post, @put, @delete("/path") | the method and path |
| method | @HttpEndpoint(url=, method=, contentType=) | the long form (PATCH too) |
| parameter | @body | the request body; {name} in the URL is a path variable; anything else a query parameter |
Runtime properties
Per port, {domainName}.{PortServiceName}.{property} in application.yml overrides the DSL - for
example erp.CreditCheckPort.url, .connectTimeout, .readTimeout, .authKey, .apiKey; global
defaults are elasticore.http.connectTimeout and .readTimeout. Proxies, SSL and multipart
form-data are described in the port guide (blueprint/elasticore-dsl-port-guide.md).
Two kinds of client
| Runtime proxy (default) | httpPort: spring (Spring Boot 4) | |
|---|---|---|
| generated | an @ExternalService interface | a Spring @HttpExchange interface + <Domain>HttpServices with @ImportHttpServices + a properties file of defaults |
| registered by | the runtime's auto-configuration (from 3.0.261023; up to 3.0.261022-rc1 @EnableElastiCore) - see Ports | Spring |
| base URL | read on every call ({id}.url) | fixed when the application starts |
elasticore:
config:
httpPort: spring # this domain's HTTP ports as Spring HTTP service clients
implementation 'org.springframework.boot:spring-boot-starter-restclient'
@HttpExchange
public interface CreditCheckPort {
@GetExchange(url = "/partners/{bizNo}/credit")
CreditResultDTO checkCredit(@PathVariable("bizNo") String bizNo);
@PostExchange(url = "/partners/{bizNo}/review")
CreditResultDTO requestReview(@PathVariable("bizNo") String bizNo, @RequestBody(required = false) CreditResultDTO body);
}
The group name is <domain>--<port> (erp--credit-check-port); the old keys still work, and the
Boot 4 keys are spring.http.serviceclient.erp--credit-check-port.base-url and so on. On Spring Boot
3 the setting warns and the port stays a runtime proxy; a few shapes (form-urlencoded, multipart
without @contentType, array parameters ...) stay on the runtime, and the build log says why.
When the other system fails
A port does not decide what is a failure. Both kinds return an error answer's body as the value:
| The other system | Runtime proxy | httpPort: spring |
|---|---|---|
| 200 with a readable body | the value | the value |
| 4xx/5xx with a JSON body | the value (missing fields null) | the value |
| 4xx/5xx with a body the return type cannot read | an exception | an exception |
| no answer - refused, timed out | an exception (WebClientRequestException ...) | RestClientException |
Used naively it succeeds wrongly: the courier answers 500, the tracking number is null, and the
order is saved as shipped with a 200. The code that calls the port decides, and throws
io.elasticore.springboot3.http.PortCallException - the generated XxxApiErrors answers 502 with
"code": "upstream":
@Override
@Transactional // the state change and the call in one transaction: undone together
public PurchaseOrderDTO ship(String orderId) {
PurchaseOrderDTO shipped = super.ship(orderId);
ShipmentReply reply;
try {
reply = courier.ship(new ShipmentRequest(orderId));
} catch (RestClientException unreachable) { // also WebClientException with the runtime proxy
throw new PortCallException("The courier could not be reached", unreachable);
}
if (reply == null || reply.getTrackingNo() == null) { // an error status comes as a value too
throw new PortCallException("The courier did not accept the shipment");
}
...
}
PortCallException is unchecked, so inside a transaction it rolls back what the method changed. Give
it a message code to have it translated (PortCallException(code, message, args...)), or keep the
other side's status and body (PortCallException(port, upstreamStatus, upstreamBody, message, cause)).
A failure policy set by configuration is not there yet.
Testing against a fake
- Replace the port with a mock:
@MockitoBean CourierApiPort courier;(Spring Boot 3.4+, before@MockBean), returning a success, an error body (nullfields) and an exception to check the 502 and the rollback. - A fake controller in the same server (
/fake-courier) needs a fixed port (webEnvironment = DEFINED_PORT): withhttpPort: springandRANDOM_PORTthe client calls port 0. - An outside fake server (MockWebServer, WireMock): put its address in
{domain}.{Port}.url.