mirror of
https://github.com/HeyPuter/puter.git
synced 2025-02-03 07:48:46 +08:00
refactor: allow service-specific API errors to be registered
This commit is contained in:
parent
8557c54527
commit
ff62d9009e
@ -324,36 +324,6 @@ module.exports = class APIError {
|
|||||||
message: () => 'Invalid token.',
|
message: () => 'Invalid token.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// drivers
|
|
||||||
'interface_not_found': {
|
|
||||||
status: 404,
|
|
||||||
message: ({ interface_name }) => `Interface not found: ${quot(interface_name)}`,
|
|
||||||
},
|
|
||||||
'no_implementation_available': {
|
|
||||||
status: 502,
|
|
||||||
message: ({
|
|
||||||
iface,
|
|
||||||
interface_name,
|
|
||||||
driver
|
|
||||||
}) => `No implementation available for ` +
|
|
||||||
(iface ?? interface_name) ? 'interface' : 'driver' +
|
|
||||||
' ' + quot(iface ?? interface_name ?? driver) + '.',
|
|
||||||
},
|
|
||||||
'method_not_found': {
|
|
||||||
status: 404,
|
|
||||||
message: ({ interface_name, method_name }) => `Method not found: ${quot(method_name)} on interface ${quot(interface_name)}`,
|
|
||||||
},
|
|
||||||
'missing_required_argument': {
|
|
||||||
status: 400,
|
|
||||||
message: ({ interface_name, method_name, arg_name }) =>
|
|
||||||
`Missing required argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}`,
|
|
||||||
},
|
|
||||||
'argument_consolidation_failed': {
|
|
||||||
status: 400,
|
|
||||||
message: ({ interface_name, method_name, arg_name, message }) =>
|
|
||||||
`Failed to parse or process argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}: ${message}`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// SLA
|
// SLA
|
||||||
'rate_limit_exceeded': {
|
'rate_limit_exceeded': {
|
||||||
status: 429,
|
status: 429,
|
||||||
|
54
src/backend/src/modules/web/APIErrorService.js
Normal file
54
src/backend/src/modules/web/APIErrorService.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
const APIError = require("../../api/APIError");
|
||||||
|
const BaseService = require("../../services/BaseService");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ErrorSpec
|
||||||
|
* @property {string} code - The error code
|
||||||
|
* @property {string} status - HTTP status code
|
||||||
|
* @property {function} message - A function that generates an error message
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The APIErrorService class provides a mechanism for registering and managing
|
||||||
|
* error codes and messages which may be sent to clients.
|
||||||
|
*
|
||||||
|
* This allows for a single source-of-truth for error codes and messages that
|
||||||
|
* are used by multiple services.
|
||||||
|
*/
|
||||||
|
class APIErrorService extends BaseService {
|
||||||
|
_construct () {
|
||||||
|
this.codes = {
|
||||||
|
...this.constructor.codes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hardcoded error codes from before this service was created
|
||||||
|
static codes = APIError.codes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers API error codes.
|
||||||
|
*
|
||||||
|
* @param {Object.<string, ErrorSpec>} codes - A map of error codes to error specifications
|
||||||
|
*/
|
||||||
|
register (codes) {
|
||||||
|
for ( const code in codes ) {
|
||||||
|
this.codes[code] = codes[code];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create (code, fields) {
|
||||||
|
const error_spec = this.codes[code];
|
||||||
|
if ( ! error_spec ) {
|
||||||
|
return new APIError(500, 'Missing error message.', null, {
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new APIError(error_spec.status, error_spec.message, null, {
|
||||||
|
...fields,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = APIErrorService;
|
@ -19,6 +19,9 @@ class WebModule extends AdvancedBase {
|
|||||||
|
|
||||||
const WebServerService = require("./WebServerService");
|
const WebServerService = require("./WebServerService");
|
||||||
services.registerService('web-server', WebServerService);
|
services.registerService('web-server', WebServerService);
|
||||||
|
|
||||||
|
const APIErrorService = require("./APIErrorService");
|
||||||
|
services.registerService('api-error', APIErrorService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ const { PermissionUtil } = require("../auth/PermissionService");
|
|||||||
const { Invoker } = require("../../../../putility/src/libs/invoker");
|
const { Invoker } = require("../../../../putility/src/libs/invoker");
|
||||||
const { get_user } = require("../../helpers");
|
const { get_user } = require("../../helpers");
|
||||||
|
|
||||||
|
const strutil = require('@heyputer/putility').libs.string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DriverService provides the functionality of Puter drivers.
|
* DriverService provides the functionality of Puter drivers.
|
||||||
* This class is responsible for managing and interacting with Puter drivers.
|
* This class is responsible for managing and interacting with Puter drivers.
|
||||||
@ -51,6 +53,50 @@ class DriverService extends BaseService {
|
|||||||
this.service_aliases = {};
|
this.service_aliases = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is responsible for calling a driver's method with provided arguments.
|
||||||
|
* It checks for permissions, selects the best option, and applies rate and monthly usage limits before invoking the driver.
|
||||||
|
*
|
||||||
|
* @param {Object} o - An object containing driver, interface, method, and arguments.
|
||||||
|
* @returns {Promise<{success: boolean, service: DriverService.Driver, result: any, metadata: any}>}
|
||||||
|
*/
|
||||||
|
_init () {
|
||||||
|
const svc_registry = this.services.get('registry');
|
||||||
|
svc_registry.register_collection('');
|
||||||
|
|
||||||
|
const { quot } = strutil;
|
||||||
|
const svc_apiError = this.services.get('api-error');
|
||||||
|
svc_apiError.register({
|
||||||
|
'missing_required_argument': {
|
||||||
|
status: 400,
|
||||||
|
message: ({ interface_name, method_name, arg_name }) =>
|
||||||
|
`Missing required argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}`,
|
||||||
|
},
|
||||||
|
'argument_consolidation_failed': {
|
||||||
|
status: 400,
|
||||||
|
message: ({ interface_name, method_name, arg_name, message }) =>
|
||||||
|
`Failed to parse or process argument ${quot(arg_name)} for method ${quot(method_name)} on interface ${quot(interface_name)}: ${message}`,
|
||||||
|
},
|
||||||
|
'interface_not_found': {
|
||||||
|
status: 404,
|
||||||
|
message: ({ interface_name }) => `Interface not found: ${quot(interface_name)}`,
|
||||||
|
},
|
||||||
|
'method_not_found': {
|
||||||
|
status: 404,
|
||||||
|
message: ({ interface_name, method_name }) => `Method not found: ${quot(method_name)} on interface ${quot(interface_name)}`,
|
||||||
|
},
|
||||||
|
'no_implementation_available': {
|
||||||
|
status: 502,
|
||||||
|
message: ({
|
||||||
|
iface,
|
||||||
|
interface_name,
|
||||||
|
driver
|
||||||
|
}) => `No implementation available for ` +
|
||||||
|
(iface ?? interface_name) ? 'interface' : 'driver' +
|
||||||
|
' ' + quot(iface ?? interface_name ?? driver) + '.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is responsible for registering collections in the service registry.
|
* This method is responsible for registering collections in the service registry.
|
||||||
@ -91,19 +137,6 @@ class DriverService extends BaseService {
|
|||||||
{ col_drivers });
|
{ col_drivers });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is responsible for calling a driver's method with provided arguments.
|
|
||||||
* It checks for permissions, selects the best option, and applies rate and monthly usage limits before invoking the driver.
|
|
||||||
*
|
|
||||||
* @param {Object} o - An object containing driver, interface, method, and arguments.
|
|
||||||
* @returns {Promise<{success: boolean, service: DriverService.Driver, result: any, metadata: any}>}
|
|
||||||
*/
|
|
||||||
_init () {
|
|
||||||
const svc_registry = this.services.get('registry');
|
|
||||||
svc_registry.register_collection('');
|
|
||||||
}
|
|
||||||
|
|
||||||
register_driver (interface_name, implementation) {
|
register_driver (interface_name, implementation) {
|
||||||
this.interface_to_implementation[interface_name] = implementation;
|
this.interface_to_implementation[interface_name] = implementation;
|
||||||
}
|
}
|
||||||
@ -235,7 +268,8 @@ class DriverService extends BaseService {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
if ( ! driver_service_exists ) {
|
if ( ! driver_service_exists ) {
|
||||||
throw APIError.create('no_implementation_available', null, { iface })
|
const svc_apiError = this.services.get('api-error');
|
||||||
|
throw svc_apiError.create('no_implementation_available', { iface });
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = this.services.get(driver);
|
const service = this.services.get(driver);
|
||||||
@ -531,16 +565,18 @@ class DriverService extends BaseService {
|
|||||||
const c_interfaces = svc_registry.get('interfaces');
|
const c_interfaces = svc_registry.get('interfaces');
|
||||||
const c_types = svc_registry.get('types');
|
const c_types = svc_registry.get('types');
|
||||||
|
|
||||||
|
const svc_apiError = this.services.get('api-error');
|
||||||
|
|
||||||
// Note: 'interface' is a strict mode reserved word.
|
// Note: 'interface' is a strict mode reserved word.
|
||||||
const interface_ = c_interfaces.get(interface_name);
|
const interface_ = c_interfaces.get(interface_name);
|
||||||
if ( ! interface_ ) {
|
if ( ! interface_ ) {
|
||||||
throw APIError.create('interface_not_found', null, { interface_name });
|
throw svc_apiError.create('interface_not_found', { interface_name });
|
||||||
}
|
}
|
||||||
|
|
||||||
const processed_args = {};
|
const processed_args = {};
|
||||||
const method = interface_.methods[method_name];
|
const method = interface_.methods[method_name];
|
||||||
if ( ! method ) {
|
if ( ! method ) {
|
||||||
throw APIError.create('method_not_found', null, { interface_name, method_name });
|
throw svc_apiError.create('method_not_found', { interface_name, method_name });
|
||||||
}
|
}
|
||||||
|
|
||||||
for ( const [arg_name, arg_descriptor] of Object.entries(method.parameters) ) {
|
for ( const [arg_name, arg_descriptor] of Object.entries(method.parameters) ) {
|
||||||
@ -551,7 +587,7 @@ class DriverService extends BaseService {
|
|||||||
// There's a particular way I want to do this that involves
|
// There's a particular way I want to do this that involves
|
||||||
// a trait for extensible behaviour.
|
// a trait for extensible behaviour.
|
||||||
if ( arg_value === undefined && arg_descriptor.required ) {
|
if ( arg_value === undefined && arg_descriptor.required ) {
|
||||||
throw APIError.create('missing_required_argument', null, {
|
throw svc_apiError.create('missing_required_argument', {
|
||||||
interface_name,
|
interface_name,
|
||||||
method_name,
|
method_name,
|
||||||
arg_name,
|
arg_name,
|
||||||
@ -564,7 +600,7 @@ class DriverService extends BaseService {
|
|||||||
processed_args[arg_name] = await arg_behaviour.consolidate(
|
processed_args[arg_name] = await arg_behaviour.consolidate(
|
||||||
ctx, arg_value, { arg_descriptor, arg_name });
|
ctx, arg_value, { arg_descriptor, arg_name });
|
||||||
} catch ( e ) {
|
} catch ( e ) {
|
||||||
throw APIError.create('argument_consolidation_failed', null, {
|
throw svc_apiError.create('argument_consolidation_failed', {
|
||||||
interface_name,
|
interface_name,
|
||||||
method_name,
|
method_name,
|
||||||
arg_name,
|
arg_name,
|
||||||
|
Loading…
Reference in New Issue
Block a user