Skip to content

Commit 907a84d

Browse files
authored
fix: improve error handling (#70)
1 parent 8accb67 commit 907a84d

File tree

5 files changed

+104
-56
lines changed

5 files changed

+104
-56
lines changed

.changeset/cold-cheetahs-search.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@litehex/node-vault': patch
3+
---
4+
5+
fix: improve error handling

src/errors.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ export class VaultError extends Error {
33
if (Array.isArray(error)) {
44
error = error.join('\n');
55
}
6-
super(`VaultError: ${error && error !== '' ? error : 'Not Found'}`);
6+
super(error && error !== '' ? error : 'Not Found');
77
}
88
}

src/utils/generate-command.ts

+83-52
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import omit from 'lodash/omit';
22
import pick from 'lodash/pick';
3-
import { SafeReturn, trySafe } from 'p-safe';
3+
import { trySafe, type SafeReturn } from 'p-safe';
44
import { z } from 'zod';
5-
import { generateRequest, ZodRequestInit, ZodResponse } from 'zod-request';
5+
import {
6+
generateRequest,
7+
ZodResponse,
8+
type ZodRequestInit,
9+
type ZodValidationError
10+
} from 'zod-request';
611

712
import { VaultError } from '@/errors';
8-
import { CommandFn, CommandInit, RequestSchema } from '@/typings';
13+
import type { CommandFn, CommandInit, RequestSchema } from '@/typings';
914

1015
import { isJson } from './json';
1116
import { removeUndefined } from './object';
@@ -16,45 +21,54 @@ export function generateCommand<Schema extends RequestSchema, RawResponse extend
1621
raw: RawResponse = false
1722
): CommandFn<Schema, RawResponse> {
1823
return async (args, options = {}) => {
19-
return trySafe(async () => {
20-
const { method = 'GET', path, client, schema } = init;
21-
const { strictSchema = true, ...opts } = options;
22-
23-
const { url: _url, input } = generateRequest(
24-
`${client.endpoint}/${client.apiVersion}${client.pathPrefix}${path}`,
25-
{
26-
method,
27-
...opts,
28-
path: !schema?.path ? undefined : pick(args || {}, Object.keys(schema.path.shape)),
29-
params: !schema?.searchParams
30-
? undefined
31-
: pick(args || {}, Object.keys(schema.searchParams.shape)),
32-
body: !schema?.body
33-
? undefined
34-
: schema.body instanceof z.ZodObject
35-
? pick(args || {}, Object.keys(schema.body.shape))
36-
: (removeUndefined(
37-
omit(
38-
args,
39-
// Potential Body Keys
40-
Object.keys(schema.searchParams?.shape || {})
41-
.concat(Object.keys(schema.path?.shape || {}))
42-
.concat(Object.keys(schema.headers?.shape || {}))
43-
)
44-
) as any),
45-
headers: removeUndefined(
46-
Object.assign(
47-
{
48-
'X-Vault-Token': client.token,
49-
'X-Vault-Namespace': client.namespace
50-
},
51-
opts.headers || {}
52-
)
53-
),
54-
schema
55-
} as ZodRequestInit<any, any>
56-
);
24+
const { method = 'GET', path, client, schema } = init;
25+
const { strictSchema = true, ...opts } = options;
26+
27+
const requestInit = {
28+
method,
29+
...opts,
30+
path: !schema?.path ? undefined : pick(args || {}, Object.keys(schema.path.shape)),
31+
params: !schema?.searchParams
32+
? undefined
33+
: pick(args || {}, Object.keys(schema.searchParams.shape)),
34+
body: !schema?.body
35+
? undefined
36+
: schema.body instanceof z.ZodObject
37+
? pick(args || {}, Object.keys(schema.body.shape))
38+
: (removeUndefined(
39+
omit(
40+
args,
41+
// Potential Body Keys
42+
Object.keys(schema.searchParams?.shape || {})
43+
.concat(Object.keys(schema.path?.shape || {}))
44+
.concat(Object.keys(schema.headers?.shape || {}))
45+
)
46+
) as any),
47+
headers: removeUndefined(
48+
Object.assign(
49+
{
50+
'X-Vault-Token': client.token,
51+
'X-Vault-Namespace': client.namespace
52+
},
53+
opts.headers || {}
54+
)
55+
),
56+
schema: Object.assign(schema, {
57+
response: z.union([
58+
schema.response ?? z.any(),
59+
z.object({
60+
errors: z.array(z.string())
61+
})
62+
])
63+
})
64+
} as ZodRequestInit<any, any>;
65+
66+
const { url: _url, input } = generateRequest(
67+
`${client.endpoint}/${client.apiVersion}${client.pathPrefix}${path}`,
68+
requestInit
69+
);
5770

71+
return trySafe(async () => {
5872
const fetcher = init.fetcher || client.fetcher || fetch;
5973

6074
const rawInit = Object.assign(input as RequestInit, {
@@ -90,30 +104,47 @@ export function generateCommand<Schema extends RequestSchema, RawResponse extend
90104

91105
if (!strictSchema || !schema.response || schema.response instanceof z.ZodAny) {
92106
if (hasJsonContentType) {
93-
return resolve(await response.json());
107+
return resolve(response, await response.json());
94108
}
95109

96-
return resolve(parseText(await response.text()));
110+
return resolve(response, parseText(await response.text()));
97111
}
98112

99-
const zr = new ZodResponse(response, schema.response);
113+
// From here it might throw a schema validation error
114+
try {
115+
const zr = new ZodResponse(response, schema.response);
100116

101-
if (hasJsonContentType) {
102-
return resolve(await zr.json());
103-
}
117+
if (hasJsonContentType) {
118+
return resolve(response, await zr.json());
119+
}
120+
121+
return resolve(response, parseText(await zr.text()));
122+
} catch (e) {
123+
if (e && e instanceof VaultError) return { error: e };
104124

105-
return resolve(parseText(await zr.text()));
125+
if (e && typeof e === 'object' && e.constructor.name === 'ZodValidationError') {
126+
const error = new VaultError('Failed to validate response schema');
127+
error.cause = (e as unknown as ZodValidationError).flatten();
128+
return { error };
129+
}
130+
131+
const error = new VaultError('Failed to parse response');
132+
error.cause = e;
133+
return { error };
134+
}
106135
});
107136
};
108137
}
109138

110-
function resolve<T>(response: any): SafeReturn<T, VaultError> {
139+
function resolve<T>(response: Response, data: any): SafeReturn<T, VaultError> {
111140
// It's a Json error response
112-
if (typeof response === 'object' && 'errors' in response) {
113-
return { error: new VaultError(response.errors) };
141+
if (typeof data === 'object' && 'errors' in data) {
142+
const error = new VaultError(data.errors);
143+
error.cause = response;
144+
return { error };
114145
}
115146

116-
return { data: response };
147+
return { data };
117148
}
118149

119150
function parseText(text: string): object | string {

tests/integration.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ describe('node-vault', () => {
2121
await sleep(2e3);
2222
});
2323

24+
it('should handle errors', async () => {
25+
const res = await vc.init({
26+
secret_shares: 1,
27+
secret_threshold: 1
28+
});
29+
expect(res)
30+
.have.property('error')
31+
.be.instanceof(VaultError)
32+
.have.property('message')
33+
.be.equal('Vault is already initialized');
34+
});
35+
2436
it('should get vault health', async () => {
2537
const { data, error } = await vc.health();
2638
expect(error).be.undefined;

tests/utils.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export async function createInstance(unsealed: boolean = true): Promise<{
1616
root_token: string;
1717
}> {
1818
launchVault();
19-
await sleep(3000);
19+
await sleep(5000);
2020

2121
const vc = new Client();
2222

@@ -26,9 +26,9 @@ export async function createInstance(unsealed: boolean = true): Promise<{
2626
});
2727
if (error) throw error;
2828

29-
const { keys, root_token } = data!;
29+
const { keys, root_token } = data;
3030

31-
await sleep(2000);
31+
await sleep(1000);
3232

3333
if (unsealed) {
3434
await vc.unseal({

0 commit comments

Comments
 (0)