Hooks
Hooks are a way to add custom code to your SDK or Terraform provider to hook into the API invocation lifecycle. This document will guide you through extending the liblab generated SDK with your custom logic using our Hooks framework.
Supported SDK languages and versions:
TypeScript Java Python C# Go Terraform PHP ✅ ✅ ✅ ✅ ✅ ✅ ✅
What are Hooks?
In some use cases, you need to extend the SDK created by liblab, and add your own custom code into the SDK at the generation process. Examples of such use cases include custom authentication or auditing requirements, or providing Terraform plan modifiers.
liblab can extend your SDKs in each of the languages generated, using our Hooks framework. The SDK hooks framework lets you hook in your code into three events in the API invocation lifecycle:
- Before Request: Code implemented in this section will run before the request. You can use the request object and update the request itself before it goes out to your API.
- After Response: Code implemented in this section will run after the call returns from your API. You can use the request objects to get context for the API call made and the response object for the response provided.
- On Error: Code implemented in this section will run in case of an API call error. You can use the request objects to get context for the API call and the exception object for information about the error that accrued.
Happy path
Error path
For Terraform you can hook into the terraform plan
command to modify the plan before it is applied. You can read more on this in our Terraform provider hooks and custom plan modifiers documentation.
When working with hooks, you can implement only the ones your code needs, before request, after response, or on error. The following code block provides examples for each hook across all supported programming languages.
- TypeScript
- Python
- Java
- C#
- Go
- PHP
export class CustomHook implements Hook {
public async beforeRequest(request: HttpRequest, params: Map<string, string>): Promise<HttpRequest> {
// Your code goes here
}
public async afterResponse(request: HttpRequest, response: HttpResponse<any>, params: Map<string, string>): Promise<HttpResponse<any>> {
// Your code goes here
}
public async onError(request: HttpRequest, response: HttpResponse<any>, params: Map<string, string>): Promise<HttpError> {
// Your code goes here
}
}
class CustomHook:
def before_request(self, request: Request, **kwargs):
# Your code goes here
def after_response(self, request: Request, response: Response, **kwargs):
# Your code goes here
def on_error(self, error: Exception, request: Request, response: Response, **kwargs):
# Your code goes here
Java v2 uses the okhttp3 library.
package com.liblab.hook;
import java.util.Map;
import okhttp3.Request;
import okhttp3.Response;
public class CustomHook implements Hook {
@Override
public Request beforeRequest(Request request, Map<String, String> additionalParameters) {
return request;
}
@Override
public Response afterResponse(
Request request, Response response, Map<String, String> additionalParameters) {
return response;
}
@Override
public void onError(
Request request, Exception exception, Map<String, String> additionalParameters) {}
}
public class CustomHook : IHook
{
public async Task<HttpRequestMessage> BeforeRequestAsync(HttpRequestMessage request)
{
}
public async Task<HttpResponseMessage> AfterResponseAsync(HttpResponseMessage response)
{
}
public async Task OnErrorAsync(HttpResponseMessage response)
{
}
}
type Hook interface {
BeforeRequest(req Request) Request
AfterResponse(req Request, resp Response) Response
OnError(req Request, resp ErrorResponse) ErrorResponse
}
class CustomHook implements HookInterface
{
public function beforeRequest(RequestInterface &$request): void
{
// Your code goes here
}
public function afterResponse(RequestInterface $request, ResponseInterface &$response): void
{
// Your code goes here
}
public function onError(RequestInterface $request, Exception $exception): void
{
// Your code goes here
}
}
When generating both a Terraform provider and a standalone Go SDK, the hooks are not shared between the Go SDK created for the Terraform provider and the standalone Go SDK.
Add hooks to your liblab project
To add the hooks framework, run the following command:
liblab hooks add
This generates a hooks
folder that you can modify to extend the SDK. To ensure it's included in the SDK, the hooks
folder must be located in the same directory as your liblab configuration file. Inside the hooks
folder, you'll find subfolders for each language specified in your configuration file:
.
├── hooks
│ ├── go
│ ├── csharp
│ ├── java
│ ├── python
│ ├── terraform
│ ├── php
│ └── typescript
├── customPlanModifiers // hooks that are triggered on the `terraform plan` command
│ ├── attributes
│ └── resources
└── liblab.config.json
Each folder contains a complete code project where you can add your custom hook code. This code is sent to liblab when the SDK is generated. The presence of the hooks
folder is all liblab requires to include hooks in your SDK.
To add hooks for a single language, run the following command:
liblab hooks add --language=<language>
If you are creating hooks for a Terraform provider, you will get an additional folder called customPlanModifiers
which contains the hooks for the internal Go SDK that is used by the Terraform provider. Access the Terraform provider hooks and custom plan modifiers documentation for additional information.
Hooks can be written for the underlying Go SDK that is used by the Terraform provider by updating the code in the hooks/terraform
folder. These hooks are written in the same way as writing hooks for the Go SDK.
You can read more about adding hooks in our CLI hooks documentation.
Implement hooks
In the hooks
folder, you will find hooks code for each of the programming languages you have generated.
- TypeScript
- Python
- Java
- C#
- Go
- PHP
typescript
├── src
│ └── hook.ts
│ └── custom-hook.ts
python
├── src
│ └── hook.py
├── test
│ └── test_hook.py
└── requirements.txt
java
├── src
│ ├── main/java/com/liblab
│ │ └── hook
│ │ ├── CustomHook.java
│ │ └── Hook.java
│ └── test/java/com/liblab
│ └── hook
│ └── CustomHookTest.java
└── pom.xml
csharp
└── CustomHook.cs
go
├── hooks
│ ├── custom_hook.go
│ └── hook.go
└── go.mod
php
├── CustomHook.php
├── HookInterface.php
└── composer.json
After response
The after response hook is called once the response has been received from the SDK. The hook receives the request object, and the response object. The response object contains the response body as JSON, the headers, and the status code.
In this hook you can modify the response body or headers, and the changes will be returned to the SDK caller.
- TypeScript
- Python
- Java
- C#
- Go
- PHP
The HttpResponse
class lives in the hook.ts
file:
export interface HttpResponse<T> {
data?: T;
metadata: HttpMetadata;
raw: ArrayBuffer;
}
The afterResponse
method is implemented in the CustomHook
class in the custom-hook.ts
file:
public async afterResponse(request: HttpRequest, response: HttpResponse<any>, params: Map<string, string>): Promise<HttpResponse<any>> {
return response;
}
The Response
class lives in the src/hook.py
file:
class Response:
def __init__(self, status, headers, body):
self.status = status
self.headers = headers
self.body = body
The after_response
method is implemented in the CustomHook
class in the src/hook.py
file:
class CustomHook:
def after_response(self, request: Request, response: Response, **kwargs):
# Your code goes here
The request and response types used is an instance of okhttp3.Request.
The afterResponse
method is implemented in the CustomHook
class in the src/main/<namespace>/hook/CustomHook.java
file:
package com.liblab.hook;
import java.util.Map;
import okhttp3.Response;
public class CustomHook implements Hook {
@Override
public Response afterResponse(
Request request, Response response, Map<String, String> additionalParameters) {
// your code here
return response;
}
}
The AfterResponseAsync
method is implemented in the CustomHook
class in the CustomHook.cs
file:
public async Task<HttpResponseMessage> AfterResponseAsync(HttpResponseMessage response)
{
// Your code goes here
}
The response
parameter is an instance of System.Net.Http.HttpResponseMessage
.
The Response
type lives in the hooks/hook.go
file:
type Response interface {
GetStatusCode() int
SetStatusCode(statusCode int)
GetHeader(header string) string
SetHeader(header string, value string)
GetBody() []byte
SetBody(body []byte)
}
The AfterResponse
method is implemented in the hooks
package in the custom_hook.go
file:
func (h *CustomHook) AfterResponse(req Request, resp Response) Response {
fmt.Printf("AfterResponse: %#v\n", resp)
return resp
}
The afterResponse
method is implemented in the CustomHook
class in the CustomHook.php
file:
class CustomHook implements HookInterface
{
public function afterResponse(RequestInterface $request, ResponseInterface &$response): void
{
// Your code goes here
}
}
The response
parameter is an instance of Psr\Http\Message\ResponseInterface
.
On error
The on error hook is called if the API returns an error, such as a 4xx status code. This hook passes the original request object for reference, along with details of the error.
- TypeScript
- Python
- Java
- C#
- Go
- PHP
The HttpError
class lives in the hook.ts
file:
export interface HttpError {
error: string;
metadata: HttpMetadata;
}
The onError
method is implemented in the CustomHook
class in the custom-hook.ts
file:
public async onError(request: HttpRequest, response: HttpResponse<any>, params: Map<string, string>): Promise<HttpError> {
return new CustomHttpError('a custom error message', response.metadata);
}
The on_error
method is implemented in the CustomHook
class in the src/hook.py
file:
class CustomHook:
def on_error(self, error: Exception, request: Request, response: Response, **kwargs):
# Your code goes here
The error is provided in the error
parameter. For example, if the API call returns a status of 401, this will be a http_exceptions.client_exceptions.UnauthorizedException
.
The original request is passed to this hook in the request
parameter. The response from the call is passed in the response
parameter.
After this hook is called, the SDK code will throw the exception so that the SDK caller can handle it.
The request type used is an instance of okhttp3.Request.
The onError
method is implemented in the CustomHook
class in the src/main/<namespace>/hook/CustomHook.java
file:
package com.liblab.hook;
import java.util.Map;
import okhttp3.Request;
public class CustomHook implements Hook {
@Override
public void onError(
Request request, Exception exception, Map<String, String> additionalParameters) {
// Your code goes here
}
}
The original request is passed to this hook in the request
parameter.
The error is provided in the exception
parameter. For example, this could be a java.io.IOException
if the request failed due to a network error, or an io.swagger.exceptions.ApiException
if the request failed due to an HTTP error response.
After this hook is called, the SDK code will throw the exception so that the SDK caller can handle it.
The OnErrorAsync
method is implemented in the CustomHook
class in the CustomHook.cs
file:
public async Task OnErrorAsync(HttpResponseMessage response)
{
// Your code goes here
}
The response from the call is passed in the response
parameter, and is an instance of System.Net.Http.HttpResponseMessage
.
After this hook is called, the SDK code will throw an exception so that the SDK caller can handle it.
The OnError
method is implemented in the hooks
package in the custom_hook.go
file:
func (h *CustomHook) OnError(req Request, resp ErrorResponse) ErrorResponse {
fmt.Printf("On Error: %#v\n", resp)
return resp
}
The original request is passed to this hook in the req
parameter.
The error is provided in the resp
parameter. This has the following type:
type ErrorResponse interface {
Error() string
GetError() error
GetStatusCode() int
SetStatusCode(statusCode int)
GetHeader(header string) string
SetHeader(header string, value string)
GetBody() []byte
SetBody(body []byte)
}
After this hook is called, the SDK code will return the error.
The onError
method is implemented in the CustomHook
class in the CustomHook.php
file:
class CustomHook implements HookInterface
{
public function onError(RequestInterface $request, Exception $exception): void
{
// Your code goes here
}
}
The original request is passed to this hook in the $request
parameter.
The error is provided in the $exception
parameter.
After this hook is called, the SDK code will throw the exception so that the SDK caller can handle it.
Hook dependencies
Each hook project has relevant files to add dependencies, for example in a Python hook there is a requirements.txt
file, in Java there is a pom.xml
. If you want to add any dependencies to your hooks code, for example an SDK to add telemetry, you must add them to the relevant file so that they can be picked up during the SDK generation process.
If you do not add dependencies to these files, the SDK generation will not know to add them and your final SDK may not build or run
Before request
The before request hook is called just before the request is sent to your API. The hook receives a request object with the URL being called, the relevant verb (GET, POST etc.), the body as JSON, and the headers.
In this hook, you can modify the request object or the headers, and the changes will be sent to your API.
- TypeScript
- Python
- Java
- C#
- Go
- PHP
The HttpRequest
class lives in the hook.ts
file:
export interface HttpRequest {
method: HttpMethod;
path: string;
headers?: Map<string, unknown>;
body?: BodyInit;
abortSignal?: AbortSignal;
queryParams?: Map<string, unknown>;
}
The beforeRequest
method is implemented in the CustomHook
class in the custom-hook.ts
file:
public async beforeRequest(request: HttpRequest, params: Map<string, string>): Promise<HttpRequest> {
return request;
}
The Request
class lives in the src/hook.py
file:
class Request:
def __init__(self, method, url, headers, body=""):
self.method = method
self.url = url
self.headers = headers
self.body = body
The before_request
method is implemented in the CustomHook
class in the src/hook.py
file:
class CustomHook:
def before_request(self, request: Request, **kwargs):
# Your code goes here
The request
type used is an instance of okhttp3.Request
.
These request objects are immutable, but you can modify the request by creating a new builder, and returning the new request:
@Override
public Request beforeRequest(Request request, Map<String, String> additionalParameters) {
Request newRequest =
request.newBuilder()
// Modify the request here
.build();
return newRequest;
}
The request type used is an instance of okhttp3.Request.
The beforeRequest
method is implemented in the CustomHook
class in the src/main/<namespace>/hook/CustomHook.java
file:
package com.liblab.hook;
import java.util.Map;
import okhttp3.Request;
public class CustomHook implements Hook {
@Override
public Request beforeRequest(Request request, Map<String, String> additionalParameters) {
// Your code goes here
return request;
}
}
The BeforeRequestAsync
method is implemented in the CustomHook
class in the CustomHook.cs
file:
public async Task<HttpRequestMessage> BeforeRequestAsync(HttpRequestMessage request)
{
// Your code goes here
}
The request
type used is an instance of System.Net.Http.HttpRequestMessage
.
The Request
type lives in the hooks/hook.go
file:
type Request interface {
GetMethod() string
SetMethod(method string)
GetBaseUrl() string
SetBaseUrl(baseUrl string)
GetPath() string
SetPath(path string)
GetHeader(header string) string
SetHeader(header string, value string)
GetPathParam(param string) string
SetPathParam(param string, value any)
GetQueryParam(param string) string
SetQueryParam(param string, value any)
GetBody() any
SetBody(body any)
}
The BeforeRequest
method is implemented in the hooks
package in the custom_hook.go
file:
type CustomHook struct{}
func (h *CustomHook) BeforeRequest(req Request) Request {
// Your code goes here
return req
}
The request
type used is an instance of Psr\Http\Message\RequestInterface
.
The beforeRequest
method is implemented in the CustomHook
class in the CustomHook.php
file:
class CustomHook implements HookInterface
{
public function beforeRequest(RequestInterface &$request): void
{
// Your code goes here
}
}
Additional constructor parameters
The constructor parameters functionality isn't available for all programming languages.
Supported SDK languages and versions:
TypeScript Python Java C# Go Terraform PHP ✅ ✅ ✅ ❌ ✅ ❌ ✅
If you need to pass additional data from the code that calls your SDK into hooks, you can enable this by adding additional constructor parameters to your SDK client. These are defined in your liblab config file.
- TypeScript
- Python
- Java
- C#
- Go
- PHP
For example, if you wanted to define a client Id and client secret for oAuth that is used by your custom hooks code to authenticate requests, you could set this option in your config file like this:
{
...
"languageOptions": {
"typescript": {
"additionalConstructorParameters": [
{
"name": "client-id",
"example": "myClientId"
},
{
"name": "client-secret",
"example": "an-example-client-secret"
}
]
}
}
...
}
This would add the following parameters to the SDK client constructor config parameter:
class SDKName {
constructor(
config: {
...
clientId?: string;
clientSecret?: string;
},
...
) {
...
}
}
These parameters are passed to the params
parameter of the hooks beforeRequest
, afterResponse
, and onError
methods. For example, in the beforeRequest
method:
export class CustomHook implements Hook {
beforeRequest(request: HttpRequest, params: Map<string, string>): HttpRequest {
// Get the client Id and secret from the params
const clientId = params.get("clientId");
const clientSecret = params.get("clientSecret");
if(!clientId || !clientSecret) {
throw new Error("clientId and clientSecret are required");
}
return request;
}
}
For example, if you wanted to define a client Id and client secret for oAuth that is used by your custom hooks code to authenticate requests, you could set this option in your config file like this:
{
...
"languageOptions": {
"python": {
"additionalConstructorParameters": [
{
"name": "client_id",
"example": "myClientId"
},
{
"name": "client_secret",
"example": "myClientSecret"
}
]
}
}
...
}
This would add the following parameters to the SDK client constructor:
class MyClient:
def __init__(
self,
client_id: str = None,
client_secret: str = None,
):
These parameters are passed to the kwargs
parameter of the hooks before_request
, after_response
, and on_error
methods. For example, in the before_request
method, you could access these parameters like this:
def before_request(self, request: Request, **kwargs):
# Get the client Id and secret from the kwargs
client_id = kwargs.get("client_id")
client_secret = kwargs.get("client_secret")
For example, if you wanted to define a client Id and client secret for oAuth that is used by your custom hooks code to authenticate requests, you could set this option in your config file like this:
{
...
"languageOptions": {
"java": {
"additionalConstructorParameters": [
{
"name": "clientId",
"example": "myClientId"
},
{
"name": "clientSecret",
"example": "myClientSecret"
}
]
}
}
...
}
This would add the following fields to the SDK's config class:
public class MySdkConfig {
...
private String clientId;
private String clientSecret;
}
These can then be set when creating the SDK client:
MySdkConfig config = MySdkConfig.builder()
.clientId("myClientId")
.clientSecret("myClientSecret")
.build();
MySdk client = new MySdk(config);
These parameters are passed to the additionalParameters
parameter of the hooks beforeRequest
, afterResponse
, and onError
methods. This parameter is a Map<String, String>
, with the keys matching the name of the additional constructor parameters. For example, in the beforeRequest
method, you could access these parameters like this:
@Override
public Request beforeRequest(Request request, Map<String, String> additionalParameters) {
// Get the client Id and secret from the additional constructor parameters
String clientId = additionalParameters.get("clientId");
String clientSecret = additionalParameters.get("clientSecret");
// Do something with the client id and secret
...
return request;
}
// config object
var config = new MySdkConfig
{
...
ClientId= "myClientId",
ClientSecret = "myClientSecret"
};
// setting the config
var client = new MySdkClient(config);
// the hooks usage
public Task<HttpRequestMessage> BeforeRequestAsync(
HttpRequestMessage request,
Dictionary<string, string?> additionalParameters
)
{
var clientId = additionalParameters["clientId"]
var clientSecret = additionalParameters["clientSecret"]
// ... do something
return Task.FromResult(request);
}
For example, if you wanted to define a client Id and client secret for oAuth that is used by your custom hooks code to authenticate requests, you could set this option in your config file like this:
{
...
"languageOptions": {
"go": {
"additionalConstructorParameters": [
{
"name": "ClientId",
"example": "myClientId"
},
{
"name": "ClientSecret",
"example": "myClientSecret"
}
]
}
}
...
}
This would add the following to the config struct:
type Config struct {
...
ClientId *string
ClientSecret *string
}
func (c *Config) SetClientId(clientId string) {
c.ClientId = &clientId
}
func (c *Config) GetClientId() string {
return *c.ClientId
}
func (c *Config) SetClientSecret(clientSecret string) {
c.ClientSecret = &clientSecret
}
func (c *Config) GetClientSecret() string {
return *c.ClientSecret
}
This is then passed to the SDK client constructor:
config := mysdkconfig.NewConfig()
config.SetClientId(clientId)
config.SetClientSecret(clientSecret)
client := mysdk.NewMySDK(config)
These parameters are passed to the params
parameter of the hooks BeforeRequest
, AfterResponse
, and OnError
methods. This parameter is a map
of key/value pairs, with the keys matching the name of the additional constructor parameters. For example, in the BeforeRequest
method, you could access these parameters like this:
func (h *CustomHook) BeforeRequest(req Request, params map[string]string) Request {
// Get the client id and secret from the hook parameters
clientId := params["ClientId"]
clientSecret := params["ClientSecret"]
// Do something with the client id and secret
...
return req
}
For example, if you wanted to define a client Id and client secret for oAuth that is used by your custom hooks code to authenticate requests, you could set this option in your config file like this:
{
...
"languageOptions": {
"php": {
"additionalConstructorParameters": [
{
"name": "clientId",
"example": "myClientId"
},
{
"name": "clientSecret",
"example": "myClientSecret"
}
]
}
}
...
}
This would add the following to the SDK client constructor:
class Client
{
public function __construct(
string $clientId = '',
string $clientSecret = ''
)
}
These parameters are passed to the params
parameter of the hooks Constructor method. This parameter is an array
of key/value pairs, with the keys matching the name of the additional constructor parameters. For example, you could access these parameters like this:
public function __construct(array $params)
{
$clientId = $params['clientId'];
$clientSecret = $params['clientSecret'];
}
Build your SDK with hooks
The next time you run the liblab build
command, liblab will upload the code in the hooks
folder to the servers, and your custom code will be automatically integrated into the generated SDKs. Any package dependencies required by your hooks will be merged with the SDK dependencies.
Make sure to add the hooks
folder to your source control. This ensures your custom hook code is always available when building your SDKs. The folder should be in the same directory as your liblab configuration file.
Remove hooks
You can remove hooks by running the liblab hooks remove
command or by manually deleting the hooks
folder. After doing this, the next time you run liblab build
, the SDK will be generated without your custom hook code.
If your configuration file includes terraform
as one of the languages, the liblab hooks remove
command will also delete the customPlanModifiers
directory.