Skip to main content
ElastiCORE 3.0 is a release candidate: 3.0.261022-rc1 on Maven Central. Projects on the 2.2 line: see the 2.2 documentation.
Version: 3.0

HTTP ports

model/port.yml
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
LevelAnnotationMeans
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@bodythe 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)
generatedan @ExternalService interfacea Spring @HttpExchange interface + <Domain>HttpServices with @ImportHttpServices + a properties file of defaults
registered bythe runtime's auto-configuration (from 3.0.261023; up to 3.0.261022-rc1 @EnableElastiCore) - see PortsSpring
base URLread on every call ({id}.url)fixed when the application starts
env.yml
elasticore:
config:
httpPort: spring # this domain's HTTP ports as Spring HTTP service clients
implementation 'org.springframework.boot:spring-boot-starter-restclient'
CreditCheckPort.java (generated with httpPort: spring)
@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 systemRuntime proxyhttpPort: spring
200 with a readable bodythe valuethe value
4xx/5xx with a JSON bodythe value (missing fields null)the value
4xx/5xx with a body the return type cannot readan exceptionan exception
no answer - refused, timed outan 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":

PurchaseOrderService (yours) - shipping calls the courier
@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 (null fields) 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): with httpPort: spring and RANDOM_PORT the client calls port 0.
  • An outside fake server (MockWebServer, WireMock): put its address in {domain}.{Port}.url.