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 v1 Python v2 C# Go Terraform ✅ ✅ ✅ ✅ ✅ ✅ ✅
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.
To implement hooks, you need to implement one or more of the three hooks - before request, after response or on error.
- TypeScript v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
export default class CustomHook implements Hook {
async beforeRequest(request: Request): Promise<void> {
// Your code goes here
}
async afterResponse(request: Request, response: Response): Promise<void> {
// Your code goes here
}
async onError(error: Exception): Promise<void> {
// Your code goes here
}
}
export default class CustomHook implements Hook {
async beforeRequest(request: Request): Promise<void> {
// Your code goes here
}
async afterResponse(request: Request, response: Response): Promise<void> {
// Your code goes here
}
async onError(error: Exception): Promise<void> {
// 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
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
public class CustomHook implements Hook {
@Override
public void beforeRequest(Request request) {
// Your code goes here
}
@Override
public void afterResponse(Request request, Response response) {
// Your code goes here
}
@Override
public void onError(Request request, Exception exception) {
// Your code goes here
}
}
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
}
}
If you are generating a Terraform provider as well as a standalone Go SDK, hooks are not shared between the Go SDK generated 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 will generate a hooks
folder which you can modify to extend the SDK. To be included in our SDK, this hooks
folder must live in the same folder as your liblab config file.
This hooks
folder will contain a folder for each of the languages specified in your config file.
.
├── hooks
│ ├── go
│ ├── csharp
│ ├── java
│ ├── python
│ ├── terraform
│ └── typescript
├── customPlanModifiers // hooks that are triggered on the `terraform plan` command
│ ├── attributes
│ └── resources
└── liblab.config.json
Each folder will contain a full code project that you can add your custom hook code to. This code is then sent to liblab when your SDK is generated. The presence of the hooks
folder is all liblab needs to generate hooks in your SDK.
You can also add hooks for a single language by running:
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. You can read more on this in our Terraform provider hooks and custom plan modifiers documentation.
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 languages you have generated.
- TypeScript v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
typescript
├── src
│ └── index.ts
├── tests
│ └── hook.spec.ts
├── package-lock.json
├── package.json
├── prettier.config.js
├── tsconfig.eslint.json
└── tsconfig.json
typescript
├── src
│ └── index.ts
├── tests
│ └── hook.spec.ts
├── package-lock.json
├── package.json
├── prettier.config.js
├── tsconfig.eslint.json
└── tsconfig.json
python
├── src
│ └── hook.py
├── test
│ └── test_hook.py
└── requirements.txt
python
├── src
│ └── hook.py
├── test
│ └── test_hook.py
└── requirements.txt
java
├── src
│ ├── main/java/<namespace>
│ │ └── hook
│ │ ├── CustomHook.java
│ │ └── model
│ │ ├── Hook.java
│ │ ├── Request.java
│ │ └── Response.java
│ └── test/java/<namespace>
│ └── hook
│ └── CustomHookTest.java
└── pom.xml
csharp
└── CustomHook.cs
go
├── hooks
│ ├── custom_hook.go
│ └── hook.go
└── go.mod
php
├── CustomHook.php
├── HookInterface.php
└── composer.json
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 v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
The Request
class lives in the src/index.ts
file:
export interface Request {
method: string;
url: string;
input?: object;
headers: object;
}
The beforeRequest
method is implemented in the CustomHook
class in the src/index.ts
file:
export default class CustomHook implements Hook {
async beforeRequest(request: Request): Promise<void> {
// Your code goes here
}
}
The Request
class lives in the src/index.ts
file:
export interface Request {
method: string;
url: string;
input?: object;
headers: object;
}
The beforeRequest
method is implemented in the CustomHook
class in the src/index.ts
file:
export default class CustomHook implements Hook {
async beforeRequest(request: Request): Promise<void> {
// Your code goes here
}
}
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
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
class lives in the src/main/<namespace>/hook/model/Request.java
file:
public class Request {
public Request(String method, String url, String body, Map<String, String> headers) {}
public String getMethod() {}
public void setMethod(String method) {}
public String getUrl() {}
public void setUrl(String url) {}
public String getBody() {}
public void setBody(String body) {}
public Map<String, String> getHeaders() {}
public void setHeaders(Map<String, String> headers) {}
}
The beforeRequest
method is implemented in the CustomHook
class in the src/main/<namespace>/hook/CustomHook.java
file:
public class CustomHook implements Hook {
@Override
public void beforeRequest(Request request) {
// Your code goes here
}
}
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
}
}
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 v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
The Response
class lives in the src/index.ts
file:
export interface Response {
data: object;
headers: object;
status: number;
}
The afterResponse
method is implemented in the CustomHook
class in the src/index.ts
file:
export default class CustomHook implements Hook {
async afterResponse(request: Request, response: Response): Promise<void> {
// Your code goes here
}
}
The Response
class lives in the src/index.ts
file:
export interface Response {
data: object;
headers: object;
status: number;
}
The afterResponse
method is implemented in the CustomHook
class in the src/index.ts
file:
export default class CustomHook implements Hook {
async afterResponse(request: Request, response: Response): Promise<void> {
// Your code goes here
}
}
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 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 Response
class lives in the src/main/<namespace>/hook/model/Response.java
file:
public class Response {
public Response(int statusCode, String body, Map<String, String> headers) {}
public int getStatusCode() {}
public void setStatusCode(int statusCode) {}
public String getBody() {}
public void setBody(String body) {}
public Map<String, String> getHeaders() {}
public void setHeaders(Map<String, String> headers) {}
}
The afterResponse
method is implemented in the CustomHook
class in the src/main/<namespace>/hook/CustomHook.java
file:
public class CustomHook implements Hook {
@Override
public void afterResponse(Request request, Response response) {
// Your code goes here
}
}
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 v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
The Exception
class lives in the src/index.ts
file:
export interface Exception extends Error {
title: string;
type?: string;
detail?: string;
instance?: string;
statusCode: number;
}
The onError
method is implemented in the CustomHook
class in the src/index.ts
file:
export default class CustomHook implements Hook {
async onError(error: Exception): Promise<void> {
// Your code goes here
}
}
The Exception
class lives in the src/index.ts
file:
export interface Exception extends Error {
title: string;
type?: string;
detail?: string;
instance?: string;
statusCode: number;
}
The onError
method is implemented in the CustomHook
class in the src/index.ts
file:
export default class CustomHook implements Hook {
async onError(error: Exception): Promise<void> {
// Your code goes here
}
}
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 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 onError
method is implemented in the CustomHook
class in the src/main/<namespace>/hook/CustomHook.java
file:
public class CustomHook implements Hook {
@Override
public void onError(Request request, Exception exception) {
// 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.
Additional constructor parameters
Supported SDK languages and versions:
TypeScript v1 TypeScript v2 Java Python v1 Python v2 C# Go PHP Terraform ❌ ❌ ❌ ✅ ❌ ❌ ✅ ✅ ❌
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 v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
Additional constructor parameters are not supported in v1 of the TypeScript SDK generation.
This feature is not currently supported in TypeScript SDK generation.
Additional constructor parameters are not supported in v1 of the Python SDK generation.
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")
This feature is not currently supported in Java SDK generation.
This feature is not currently supported during the C# beta.
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
When you next run the liblab build
command, liblab will send the code in the hooks
folder to our servers, and the resulting SDKs will have your code integrated automatically into the SDKs. Any package dependencies that your hooks have will be merged with the SDK dependencies.
Remember to add the hooks folder to your source control, so that your hooks code is always available when you build your SDKs. It should live in the same folder in the same repo as your liblab config file.
Remove hooks
To remove hooks you can use the command liblab hooks remove
, or manually delete your hooks folder. Once this is done, the next time you run liblab build
, your SDK will be generated without your custom hook code.
If you have terraform
as one of the languages in your config file, this command will also delete the customPlanModifiers
directory.