celest-dev / celest

The Flutter cloud platform
https://celest.dev
Other
231 stars 13 forks source link

[RFC] Customizing HTTP behavior #136

Open dnys1 opened 1 month ago

dnys1 commented 1 month ago

This is a request-for-comment regarding the ability to customize the HTTP API exposed by Celest Functions.


In cases where Celest's standard HTTP conventions do not align with your requirements, you can control the HTTP API generated for your Celest Functions using the HTTP annotations.

Execution Controls

The @http and @httpError annotations allow you to control the behavior of the HTTP endpoint, such as how the endpoint is exposed, and how it responds in both success and error cases.

@http

To configure the method and default status code of your HTTP endpoint, use the @http annotation.

@http(
  method: HttpMethod.put, 
  statusCode: HttpStatus.created,
)
Future<String> greet(String name) async {
  return 'Hello, $name';
}

No changes are made to the generated client interface, only its execution.

final result = await celest.functions.greet('Celest');
PUT /greet
Content-Type: application/json

{
  "name": "Celest"
}
HTTP/1.1 201 Created
Content-Type: application/json

{
  "response": "Hello, Celest"
}

@httpError

To control the status code returned from one or more thrown types, use the @httpError annotation.

@httpError(403, UnauthorizedException)
@httpError(404, BadNameException, NotFoundException)
Future<String> greet(String name) async {
  if (name.isEmpty) {
    throw BadNameException();
  }
  if (!name.startsWith('C')) {
    throw NotFoundException();
  }
  if (name != 'Celest') {
    throw UnauthorizedException();
  }
  return 'Hello, $name';
}

Exception types are handled in order of specificity. Specifying a type that other types inherit from or implement will only apply the status code to subtypes which are not already covered by a more specific case.

For example, the following will return a 404 status code for both the concrete NotFoundException type and BadNameException (since it is a subtype of Exception), but not UnauthorizedException which is covered by a more specific control.

@httpError(404, NotFoundException, Exception)
@httpError(403, UnauthorizedException)
Future<String> greet(String name) async {
  if (name.isEmpty) {
    throw BadNameException(); // 404
  }
  if (!name.startsWith('C')) {
    throw NotFoundException(); // 404
  }
  if (name != 'Celest') {
    throw UnauthorizedException(); // 403
  }
  return 'Hello, $name';
}

Request Controls

The @httpHeader and @httpQuery annotations allow you to map input parameters to HTTP headers and query parameters, respectively.

Any parameters which are not targeted by these annotations are passed as the request body.

[!NOTE] Some combinations are disallowed. For example, if the @http annotation specifies a GET method, then all input parameters must be mapped, or there must be no input parameters, such that the request body is empty.

@httpHeader

To map an input parameter to an HTTP header, use the @httpHeader annotation.

[!NOTE] Only parameters of type String, int, double, bool, DateTime, or a List of these types, can be targeted by @httpHeader.

Future<String> greet(
  @httpHeader('x-api-key') String apiKey,
  String name,
) async {
  return 'Hello, $name';
}

In the generated client, these parameters are passed transparently as function arguments.

final result = await celest.functions.greet('123', 'Celest');
POST /greet
Content-Type: application/json
x-api-key: 123

{
  "name": "Celest"
}
HTTP/1.1 200 OK
Content-Type: application/json

{
  "response": "Hello, Celest"
}

The following HTTP headers are reserved and cannot be targeted by @httpHeader:

Header Name
Connection
Content-Length
Expect
Host
Max-Forwards
Server
TE
Trailer
Transfer-Encoding
Upgrade
User-Agent
Via
X-Forwarded-For

@httpQuery

To map an input parameter to a query parameter, use the @httpQuery annotation.

[!NOTE] Only parameters of type String, int, double, bool, DateTime, or a List of these types, can be targeted by @httpQuery.

@http(method: HttpMethod.get)
Future<String> greet(
  @httpQuery('name') String name,
) async {
  return 'Hello, $name';
}

In the generated client, these parameters are passed transparently as function arguments.

final result = await celest.functions.greet('Celest');
GET /greet?name=Celest
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json

{
  "response": "Hello, Celest"
}
dnys1 commented 1 month ago

This feature has been released in Celest 0.4.0! You can read all the details here: https://www.celest.dev/docs/functions/http/customization

I'll leave this issue open to collect feedback as you explore it 🚀