关于 scripted-rest-apis
面向 ServiceNow 开发者、集成专员和管理员的技能,讲解如何构建自定义 Scripted REST API,涵盖端点设计、请求与响应、安全、版本控制和测试。
主要功能
- 01自定义 REST 端点设计
- 02请求与响应格式处理
- 03身份验证与访问控制
- 04查询参数和路径参数处理
- 05错误处理与 API 版本控制
- 06API Explorer 与 curl 测试
适用场景
- 01向外部系统提供专用端点
- 02在 API 操作中实现自定义业务逻辑
- 03聚合多个表的数据
- 04定义带访问控制的版本化 API
MCP 相关
指南通过 MCP 工具设置 ServiceNow 应用上下文,并创建、查询和更新 API 定义及操作。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ happy-technologies-llc/ happy-platform-skills --skill scripted-rest-apis安装后在 Claude Code 中输入/scripted-rest-apis确认已加载。
详细介绍
脚本化 REST API 开发
概述
本技能提供了在 ServiceNow 中开发自定义脚本化 REST API 的全面指南。脚本化 REST API 可用于:
- 公开自定义端点 - 创建超出标准 Table API 范围的专用 API
- 控制数据转换 - 调整请求和响应格式,以满足集成需求
- 实现业务逻辑 - 在单次 API 调用中执行复杂操作
- 保护集成 - 应用细粒度的身份验证和授权
- 支持外部系统 - 为第三方系统集成提供接口
适用场景:
- 标准 Table API 无法满足需求
- 需要在 API 层实现自定义业务逻辑
- 需要特定的请求和响应格式
- 外部系统需要专用端点
- 需要汇总多个表中的数据
适用对象: 构建自定义 ServiceNow 集成的开发人员、集成专家和管理员。
先决条件
- 角色:
admin、web_service_admin或作用域应用开发人员 - 权限: 创建/修改 sys_ws_definition、sys_ws_operation
- 知识: JavaScript、REST 原则、HTTP 方法、JSON
- 环境: 开发实例(切勿直接在生产环境中开发 API)
- 相关技能:
admin/update-set-management- 捕获 API 定义admin/application-scope- 作用域应用 API 开发security/acl-management- API 安全控制
表架构
了解底层表对于以编程方式创建 API 至关重要。
核心表
sys_ws_definition (API Definition)
|
+-- sys_ws_operation (Resources/Endpoints)
|
+-- Request Headers (configured in operation)
+-- Query Parameters (configured in operation)
+-- Path Parameters (defined in relative_path)
+-- Script (handles request/response)
主要关系
| 表 | 用途 | 关键字段 |
|---|---|---|
sys_ws_definition | API 容器 | name、namespace、base_path、active |
sys_ws_operation | 单个端点 | name、http_method、relative_path、script |
sys_script_include | 可复用逻辑 | name、script、api_name |
sys_ws_api_header | 自定义标头 | name、api_id、direction |
API URL 结构
https://<instance>.service-now.com/api/<namespace>/<api_name>/<version>/<resource>/<path_param>
Example:
https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/12345
| | | | | |
Instance Namespace API Name Version Resource Path Param
操作步骤
阶段 1:API 设计
创建 API 之前,请仔细规划其结构。
步骤 1.1:定义 API 需求
记录 API 规范:
API Name: Customer Integration API
Namespace: x_company (scoped app) or now (global)
Version: v1
Base Path: /api/x_company/customer_api
Resources:
- GET /customers - List all customers (with pagination)
- GET /customers/{id} - Get specific customer
- POST /customers - Create new customer
- PUT /customers/{id} - Update customer
- DELETE /customers/{id} - Delete customer
- POST /customers/{id}/orders - Create order for customer
步骤 1.2:规划身份验证
| 身份验证类型 | 使用场景 | 配置 |
|---|---|---|
| Basic Auth | 服务帐户 | 默认启用 |
| OAuth 2.0 | 第三方应用 | 配置 OAuth 提供程序 |
| API Key | 简单集成 | 自定义标头验证 |
| Session | 基于浏览器 | 基于 Cookie 的身份验证 |
| Mutual TLS | 高安全性 | 证书验证 |
阶段 2:创建 API 定义
步骤 2.1:设置上下文(作用域应用)
如果在作用域应用中创建:
使用 MCP:
Tool: SN-Set-Current-Application
Parameters:
app_sys_id: [your_app_sys_id]
Tool: SN-Set-Update-Set
Parameters:
update_set_sys_id: [your_update_set_sys_id]
步骤 2.2:创建 API 定义
使用 MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_definition
data:
name: Customer Integration API
short_description: API for customer management and integration
namespace: x_company
doc_link: https://docs.company.com/api/customers
active: true
enforce_acl: true
is_versioned: true
baseline_version: v1
published: false
关键字段说明:
| 字段 | 用途 | 建议 |
|---|---|---|
namespace | URL 路径段,通常为应用作用域 | 使用应用作用域(x_company) |
enforce_acl | 检查底层表上的 ACL | 设为 true 以确保安全 |
is_versioned | 启用 API 版本控制 | 始终设为 true |
baseline_version | 未指定版本时使用的默认版本 | 从 v1 开始 |
published | 使 API 可供发现 | 开发期间设为 false |
保存 sys_id: api_definition_sys_id
步骤 2.3:验证 API 创建情况
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_definition
query: name=Customer Integration API
fields: sys_id,name,namespace,active,service_address
service_address 字段显示完整的 API URL。
阶段 3:创建资源(操作)
步骤 3.1:GET 集合资源(列表)
创建一个用于分页列出客户的端点。
使用 MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: List Customers
web_service_definition: [api_definition_sys_id]
http_method: GET
relative_path: /customers
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Get query parameters for pagination and filtering
var limit = parseInt(request.queryParams.limit) || 20;
var offset = parseInt(request.queryParams.offset) || 0;
var active = request.queryParams.active;
var search = request.queryParams.q;
// Validate limit
if (limit > 100) limit = 100;
if (limit < 1) limit = 20;
// Build query
var gr = new GlideRecord('customer');
if (active !== undefined) {
gr.addQuery('active', active === 'true');
}
if (search) {
var qc = gr.addQuery('name', 'CONTAINS', search);
qc.addOrCondition('email', 'CONTAINS', search);
qc.addOrCondition('account_number', 'CONTAINS', search);
}
// Get total count before pagination
var countGR = new GlideAggregate('customer');
countGR.addAggregate('COUNT');
if (active !== undefined) {
countGR.addQuery('active', active === 'true');
}
countGR.query();
var totalCount = 0;
if (countGR.next()) {
totalCount = parseInt(countGR.getAggregate('COUNT'));
}
// Apply pagination
gr.orderBy('name');
gr.chooseWindow(offset, offset + limit);
gr.query();
// Build response
var customers = [];
while (gr.next()) {
customers.push({
sys_id: gr.getUniqueValue(),
name: gr.getValue('name'),
email: gr.getValue('email'),
account_number: gr.getValue('account_number'),
active: gr.getValue('active') === 'true',
created: gr.getValue('sys_created_on'),
updated: gr.getValue('sys_updated_on')
});
}
// Set response
response.setStatus(200);
response.setBody({
result: customers,
meta: {
total: totalCount,
limit: limit,
offset: offset,
has_more: (offset + limit) < totalCount
}
});
})(request, response);
short_description: Retrieve list of customers with pagination
requires_acl_authorization: true
requires_authentication: true
requires_snc_internal_role: false
保存 sys_id: list_customers_operation_id
步骤 3.2:GET 单个资源
创建一个用于检索指定客户的端点。
使用 MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Get Customer
web_service_definition: [api_definition_sys_id]
http_method: GET
relative_path: /customers/{id}
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Get path parameter
var customerId = request.pathParams.id;
// Validate input
if (!customerId) {
response.setStatus(400);
response.setBody({
error: {
message: 'Customer ID is required',
code: 'MISSING_PARAMETER'
}
});
return;
}
// Query customer
var gr = new GlideRecord('customer');
// Support both sys_id and account_number lookup
if (customerId.length === 32) {
gr.get(customerId);
} else {
gr.addQuery('account_number', customerId);
gr.query();
gr.next();
}
if (!gr.isValidRecord()) {
response.setStatus(404);
response.setBody({
error: {
message: 'Customer not found',
code: 'NOT_FOUND',
detail: 'No customer exists with ID: ' + customerId
}
});
return;
}
// Check ACL (optional if enforce_acl is true on definition)
if (!gr.canRead()) {
response.setStatus(403);
response.setBody({
error: {
message: 'Access denied',
code: 'FORBIDDEN'
}
});
return;
}
// Build detailed response
var customer = {
sys_id: gr.getUniqueValue(),
name: gr.getValue('name'),
email: gr.getValue('email'),
phone: gr.getValue('phone'),
account_number: gr.getValue('account_number'),
address: {
street: gr.getValue('street'),
city: gr.getValue('city'),
state: gr.getValue('state'),
zip: gr.getValue('zip'),
country: gr.getValue('country')
},
contacts: [],
active: gr.getValue('active') === 'true',
created: gr.getValue('sys_created_on'),
updated: gr.getValue('sys_updated_on'),
created_by: gr.getDisplayValue('sys_created_by'),
updated_by: gr.getDisplayValue('sys_updated_by')
};
// Get related contacts
var contacts = new GlideRecord('customer_contact');
contacts.addQuery('customer', gr.getUniqueValue());
contacts.query();
while (contacts.next()) {
customer.contacts.push({
sys_id: contacts.getUniqueValue(),
name: contacts.getValue('name'),
email: contacts.getValue('email'),
phone: contacts.getValue('phone'),
primary: contacts.getValue('primary') === 'true'
});
}
response.setStatus(200);
response.setBody({ result: customer });
})(request, response);
short_description: Retrieve a specific customer by ID
requires_acl_authorization: true
requires_authentication: true
步骤 3.3:POST 资源(创建)
创建一个用于创建新客户的端点。
使用 MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Create Customer
web_service_definition: [api_definition_sys_id]
http_method: POST
relative_path: /customers
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Parse request body
var body = request.body.data;
// Validate required fields
var errors = [];
if (!body.name) errors.push('name is required');
if (!body.email) errors.push('email is required');
// Validate email format
if (body.email && !body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
errors.push('email format is invalid');
}
if (errors.length > 0) {
response.setStatus(400);
response.setBody({
error: {
message: 'Validation failed',
code: 'VALIDATION_ERROR',
details: errors
}
});
return;
}
// Check for duplicate email
var existing = new GlideRecord('customer');
existing.addQuery('email', body.email);
existing.query();
if (existing.next()) {
response.setStatus(409);
response.setBody({
error: {
message: 'Customer with this email already exists',
code: 'DUPLICATE_ERROR',
existing_id: existing.getUniqueValue()
}
});
return;
}
// Create customer
var gr = new GlideRecord('customer');
gr.initialize();
// Required fields
gr.setValue('name', body.name);
gr.setValue('email', body.email);
// Optional fields
if (body.phone) gr.setValue('phone', body.phone);
if (body.account_number) gr.setValue('account_number', body.account_number);
if (body.address) {
if (body.address.street) gr.setValue('street', body.address.street);
if (body.address.city) gr.setValue('city', body.address.city);
if (body.address.state) gr.setValue('state', body.address.state);
if (body.address.zip) gr.setValue('zip', body.address.zip);
if (body.address.country) gr.setValue('country', body.address.country);
}
gr.setValue('active', true);
// Check create permission
if (!gr.canCreate()) {
response.setStatus(403);
response.setBody({
error: {
message: 'Permission denied to create customer',
code: 'FORBIDDEN'
}
});
return;
}
var sysId = gr.insert();
if (sysId) {
// Set Location header for created resource
response.setHeader('Location', request.url + '/' + sysId);
response.setStatus(201);
response.setBody({
result: {
sys_id: sysId,
message: 'Customer created successfully'
}
});
} else {
response.setStatus(500);
response.setBody({
error: {
message: 'Failed to create customer',
code: 'INTERNAL_ERROR'
}
});
}
})(request, response);
short_description: Create a new customer
requires_acl_authorization: true
requires_authentication: true
第 3.4 步:PUT 资源(更新)
创建一个用于更新现有客户的端点。
使用 MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Update Customer
web_service_definition: [api_definition_sys_id]
http_method: PUT
relative_path: /customers/{id}
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var customerId = request.pathParams.id;
var body = request.body.data;
// Find customer
var gr = new GlideRecord('customer');
if (!gr.get(customerId)) {
response.setStatus(404);
response.setBody({
error: {
message: 'Customer not found',
code: 'NOT_FOUND'
}
});
return;
}
// Check update permission
if (!gr.canWrite()) {
response.setStatus(403);
response.setBody({
error: {
message: 'Permission denied to update customer',
code: 'FORBIDDEN'
}
});
return;
}
// Validate email if being changed
if (body.email && body.email !== gr.getValue('email')) {
if (!body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
response.setStatus(400);
response.setBody({
error: {
message: 'Invalid email format',
code: 'VALIDATION_ERROR'
}
});
return;
}
// Check for duplicate
var existing = new GlideRecord('customer');
existing.addQuery('email', body.email);
existing.addQuery('sys_id', '!=', customerId);
existing.query();
if (existing.next()) {
response.setStatus(409);
response.setBody({
error: {
message: 'Another customer with this email exists',
code: 'DUPLICATE_ERROR'
}
});
return;
}
}
// Update fields
var updatedFields = [];
if (body.name !== undefined) {
gr.setValue('name', body.name);
updatedFields.push('name');
}
if (body.email !== undefined) {
gr.setValue('email', body.email);
updatedFields.push('email');
}
if (body.phone !== undefined) {
gr.setValue('phone', body.phone);
updatedFields.push('phone');
}
if (body.active !== undefined) {
gr.setValue('active', body.active);
updatedFields.push('active');
}
if (body.address) {
if (body.address.street !== undefined) gr.setValue('street', body.address.street);
if (body.address.city !== undefined) gr.setValue('city', body.address.city);
if (body.address.state !== undefined) gr.setValue('state', body.address.state);
if (body.address.zip !== undefined) gr.setValue('zip', body.address.zip);
if (body.address.country !== undefined) gr.setValue('country', body.address.country);
updatedFields.push('address');
}
gr.update();
response.setStatus(200);
response.setBody({
result: {
sys_id: customerId,
message: 'Customer updated successfully',
updated_fields: updatedFields
}
});
})(request, response);
short_description: Update an existing customer
requires_acl_authorization: true
requires_authentication: true
第 3.5 步:DELETE 资源
创建一个用于删除客户的端点。
使用 MCP:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Delete Customer
web_service_definition: [api_definition_sys_id]
http_method: DELETE
relative_path: /customers/{id}
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var customerId = request.pathParams.id;
// Find customer
var gr = new GlideRecord('customer');
if (!gr.get(customerId)) {
response.setStatus(404);
response.setBody({
error: {
message: 'Customer not found',
code: 'NOT_FOUND'
}
});
return;
}
// Check delete permission
if (!gr.canDelete()) {
response.setStatus(403);
response.setBody({
error: {
message: 'Permission denied to delete customer',
code: 'FORBIDDEN'
}
});
return;
}
// Soft delete vs hard delete decision
// Option 1: Soft delete (preferred)
gr.setValue('active', false);
gr.update();
// Option 2: Hard delete (use with caution)
// gr.deleteRecord();
response.setStatus(200);
response.setBody({
result: {
sys_id: customerId,
message: 'Customer deactivated successfully'
}
});
// Alternative: Return 204 No Content for true DELETE
// response.setStatus(204);
})(request, response);
short_description: Delete (deactivate) a customer
requires_acl_authorization: true
requires_authentication: true
第 4 阶段:身份验证与安全
第 4.1 步:配置 API 级身份验证
API 定义控制身份验证要求:
Tool: SN-Update-Record
Parameters:
table_name: sys_ws_definition
sys_id: [api_definition_sys_id]
data:
requires_authentication: true
enforce_acl: true
acl_failure_result: UNAUTHORIZED
第 4.2 步:实现 API 密钥身份验证
对于简单集成,实现 API 密钥验证:
创建 API 密钥脚本包含项:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: CustomerAPIAuth
script: |
var CustomerAPIAuth = Class.create();
CustomerAPIAuth.prototype = {
initialize: function() {
this.API_KEY_HEADER = 'X-API-Key';
this.API_KEY_TABLE = 'x_company_api_keys';
},
validateApiKey: function(request) {
var apiKey = request.getHeader(this.API_KEY_HEADER);
if (!apiKey) {
return {
valid: false,
error: 'API key required',
code: 'MISSING_API_KEY'
};
}
// Look up API key
var keyRecord = new GlideRecord(this.API_KEY_TABLE);
keyRecord.addQuery('key_value', apiKey);
keyRecord.addQuery('active', true);
keyRecord.addQuery('expires', '>', new GlideDateTime());
keyRecord.query();
if (!keyRecord.next()) {
return {
valid: false,
error: 'Invalid or expired API key',
code: 'INVALID_API_KEY'
};
}
// Update last used timestamp
keyRecord.setValue('last_used', new GlideDateTime());
keyRecord.update();
return {
valid: true,
client_id: keyRecord.getValue('client_id'),
permissions: keyRecord.getValue('permissions').split(','),
rate_limit: parseInt(keyRecord.getValue('rate_limit')) || 1000
};
},
type: 'CustomerAPIAuth'
};
api_name: CustomerAPIAuth
active: true
access: public
在 API 操作中使用:
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Validate API key
var auth = new CustomerAPIAuth();
var authResult = auth.validateApiKey(request);
if (!authResult.valid) {
response.setStatus(401);
response.setBody({
error: {
message: authResult.error,
code: authResult.code
}
});
return;
}
// Check permission
if (authResult.permissions.indexOf('read:customers') === -1) {
response.setStatus(403);
response.setBody({
error: {
message: 'Insufficient permissions',
code: 'FORBIDDEN',
required: 'read:customers'
}
});
return;
}
// Continue with request processing...
})(request, response);
第 4.3 步:实现速率限制
添加速率限制以保护 API:
创建速率限制器脚本包含项:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: APIRateLimiter
script: |
var APIRateLimiter = Class.create();
APIRateLimiter.prototype = {
initialize: function(clientId, rateLimit) {
this.clientId = clientId;
this.rateLimit = rateLimit || 100; // requests per minute
this.CACHE_PREFIX = 'api_rate_';
},
isAllowed: function() {
var cacheKey = this.CACHE_PREFIX + this.clientId;
var cache = new GlideSysCache('APIRateLimit');
// Get current count
var countStr = cache.get(cacheKey);
var count = countStr ? parseInt(countStr) : 0;
if (count >= this.rateLimit) {
return {
allowed: false,
remaining: 0,
reset_in: 60
};
}
// Increment count with 60 second TTL
cache.put(cacheKey, String(count + 1), 60);
return {
allowed: true,
remaining: this.rateLimit - count - 1,
limit: this.rateLimit
};
},
type: 'APIRateLimiter'
};
api_name: APIRateLimiter
active: true
在 API 中使用速率限制:
// After authentication
var rateLimiter = new APIRateLimiter(authResult.client_id, authResult.rate_limit);
var rateResult = rateLimiter.isAllowed();
response.setHeader('X-RateLimit-Limit', rateResult.limit || authResult.rate_limit);
response.setHeader('X-RateLimit-Remaining', rateResult.remaining);
if (!rateResult.allowed) {
response.setStatus(429);
response.setHeader('Retry-After', rateResult.reset_in);
response.setBody({
error: {
message: 'Rate limit exceeded',
code: 'RATE_LIMITED',
retry_after: rateResult.reset_in
}
});
return;
}
第 5 阶段:查询参数和路径参数
第 5.1 步:常见查询参数模式
// Pagination
var limit = Math.min(parseInt(request.queryParams.limit) || 20, 100);
var offset = parseInt(request.queryParams.offset) || 0;
var page = parseInt(request.queryParams.page) || 1;
offset = (page - 1) * limit; // If using page instead of offset
// Sorting
var sortField = request.queryParams.sort || 'name';
var sortOrder = request.queryParams.order || 'asc';
var validSortFields = ['name', 'email', 'created', 'updated'];
if (validSortFields.indexOf(sortField) === -1) {
sortField = 'name';
}
if (sortOrder === 'desc') {
gr.orderByDesc(sortField);
} else {
gr.orderBy(sortField);
}
// Filtering
var filters = {
active: request.queryParams.active,
category: request.queryParams.category,
created_after: request.queryParams.created_after,
created_before: request.queryParams.created_before
};
if (filters.active !== undefined) {
gr.addQuery('active', filters.active === 'true');
}
if (filters.category) {
gr.addQuery('category', filters.category);
}
if (filters.created_after) {
gr.addQuery('sys_created_on', '>=', filters.created_after);
}
if (filters.created_before) {
gr.addQuery('sys_created_on', '<=', filters.created_before);
}
// Field selection
var fields = request.queryParams.fields;
if (fields) {
var fieldList = fields.split(',');
// Validate fields before use
var validFields = ['sys_id', 'name', 'email', 'phone', 'active'];
fieldList = fieldList.filter(function(f) {
return validFields.indexOf(f.trim()) !== -1;
});
}
// Search/Text filter
var searchQuery = request.queryParams.q || request.queryParams.search;
if (searchQuery) {
var qc = gr.addQuery('name', 'CONTAINS', searchQuery);
qc.addOrCondition('description', 'CONTAINS', searchQuery);
}
第 5.2 步:路径参数模式
// Single path parameter
// URL: /customers/{id}
var customerId = request.pathParams.id;
// Multiple path parameters
// URL: /customers/{customer_id}/orders/{order_id}
var customerId = request.pathParams.customer_id;
var orderId = request.pathParams.order_id;
// Validate path parameters
if (!customerId || customerId.length < 1) {
response.setStatus(400);
response.setBody({
error: {
message: 'Invalid customer ID',
code: 'INVALID_PARAMETER'
}
});
return;
}
// Support multiple ID formats
function resolveCustomerId(id) {
// sys_id (32 char hex)
if (/^[a-f0-9]{32}$/i.test(id)) {
return { field: 'sys_id', value: id };
}
// Account number (numeric or alphanumeric)
if (/^[A-Z]{2}\d{6}$/.test(id)) {
return { field: 'account_number', value: id };
}
// Email
if (id.indexOf('@') !== -1) {
return { field: 'email', value: id };
}
return null;
}
var idInfo = resolveCustomerId(customerId);
if (!idInfo) {
response.setStatus(400);
response.setBody({
error: {
message: 'Unrecognized ID format',
code: 'INVALID_ID_FORMAT',
supported_formats: ['sys_id', 'account_number (XX000000)', 'email']
}
});
return;
}
第 6 阶段:错误处理和状态码
第 6.1 步:标准错误响应格式
// Error response helper
function sendError(response, status, message, code, details) {
response.setStatus(status);
var error = {
error: {
message: message,
code: code,
timestamp: new GlideDateTime().getValue()
}
};
if (details) {
error.error.details = details;
}
response.setBody(error);
}
// Usage examples
sendError(response, 400, 'Invalid request body', 'BAD_REQUEST');
sendError(response, 401, 'Authentication required', 'UNAUTHORIZED');
sendError(response, 403, 'Permission denied', 'FORBIDDEN');
sendError(response, 404, 'Resource not found', 'NOT_FOUND');
sendError(response, 409, 'Conflict with existing resource', 'CONFLICT');
sendError(response, 422, 'Validation failed', 'VALIDATION_ERROR', errors);
sendError(response, 429, 'Rate limit exceeded', 'RATE_LIMITED');
sendError(response, 500, 'Internal server error', 'INTERNAL_ERROR');
sendError(response, 503, 'Service unavailable', 'SERVICE_UNAVAILABLE');
第 6.2 步:全面的错误处理
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
try {
// Wrap entire request processing in try-catch
// Validate content type for POST/PUT
if (['POST', 'PUT', 'PATCH'].indexOf(request.getMethod()) !== -1) {
var contentType = request.getHeader('Content-Type');
if (!contentType || contentType.indexOf('application/json') === -1) {
response.setStatus(415);
response.setBody({
error: {
message: 'Content-Type must be application/json',
code: 'UNSUPPORTED_MEDIA_TYPE'
}
});
return;
}
}
// Parse body with error handling
var body;
try {
body = request.body.data;
if (!body && request.body.dataString) {
body = JSON.parse(request.body.dataString);
}
} catch (parseError) {
response.setStatus(400);
response.setBody({
error: {
message: 'Invalid JSON in request body',
code: 'PARSE_ERROR',
detail: parseError.message
}
});
return;
}
// Main processing logic here...
} catch (e) {
// Log error for debugging
gs.error('[CustomerAPI] Unhandled error: ' + e.message + '\nStack: ' + e.stack);
response.setStatus(500);
response.setBody({
error: {
message: 'An unexpected error occurred',
code: 'INTERNAL_ERROR',
reference: gs.generateGUID() // For support reference
}
});
}
})(request, response);
第 6.3 步:HTTP 状态码参考
| 状态 | 含义 | 使用场景 |
|---|---|---|
| 200 | 成功 | GET、PUT、PATCH、DELETE 请求成功 |
| 201 | 已创建 | POST 请求成功创建新资源 |
| 204 | 无内容 | DELETE 请求成功且无响应正文 |
| 400 | 错误请求 | 语法无效、缺少必填字段 |
| 401 | 未经授权 | 缺少身份验证或身份验证无效 |
| 403 | 禁止访问 | 已通过身份验证,但未获授权 |
| 404 | 未找到 | 资源不存在 |
| 405 | 方法不允许 | 端点不支持该 HTTP 方法 |
| 409 | 冲突 | 资源重复、状态冲突 |
| 415 | 不支持的媒体类型 | Content-Type 标头错误 |
| 422 | 无法处理的实体 | 验证错误 |
| 429 | 请求过多 | 超出速率限制 |
| 500 | 内部服务器错误 | 意外的服务器错误 |
| 503 | 服务不可用 | 临时维护或过载 |
第 7 阶段:API 版本控制
第 7.1 步:版本配置
更新 API 定义以支持版本控制:
Tool: SN-Update-Record
Parameters:
table_name: sys_ws_definition
sys_id: [api_definition_sys_id]
data:
is_versioned: true
baseline_version: v1
deprecated_versions: v1-beta
第 7.2 步:特定版本的操作
创建特定版本的端点:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: List Customers v2
web_service_definition: [api_definition_sys_id]
http_method: GET
relative_path: /v2/customers
operation_script: |
// V2 version with enhanced response format
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// V2 includes additional fields and different structure
var customers = [];
var gr = new GlideRecord('customer');
gr.query();
while (gr.next()) {
customers.push({
id: gr.getUniqueValue(),
attributes: {
name: gr.getValue('name'),
email: gr.getValue('email'),
phone: gr.getValue('phone')
},
relationships: {
contacts: '/api/x_company/customer_api/v2/customers/' + gr.getUniqueValue() + '/contacts',
orders: '/api/x_company/customer_api/v2/customers/' + gr.getUniqueValue() + '/orders'
},
meta: {
created: gr.getValue('sys_created_on'),
updated: gr.getValue('sys_updated_on')
}
});
}
response.setStatus(200);
response.setBody({
data: customers,
links: {
self: request.url,
next: null,
prev: null
}
});
})(request, response);
第 7.3 步:弃用标头
// Add deprecation headers for old versions
if (request.url.indexOf('/v1/') !== -1) {
response.setHeader('Deprecation', 'true');
response.setHeader('Sunset', 'Sat, 01 Jul 2027 00:00:00 GMT');
response.setHeader('Link', '</api/x_company/customer_api/v2/customers>; rel="successor-version"');
}
第 8 阶段:文档和测试
第 8.1 步:集成 API Explorer
ServiceNow 会自动生成 API Explorer 文档。对其进行完善:
Tool: SN-Update-Record
Parameters:
table_name: sys_ws_definition
sys_id: [api_definition_sys_id]
data:
doc_link: https://docs.company.com/api/customers
short_description: |
Customer management API for external integrations.
Authentication: Basic Auth or API Key (X-API-Key header)
Rate Limit: 100 requests per minute
Base URL: https://instance.service-now.com/api/x_company/customer_api/v1
第 8.2 步:使用 curl 测试
GET 请求:
# List customers
curl -X GET \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers?limit=10&offset=0" \
-H "Accept: application/json" \
-H "Authorization: Basic $(echo -n 'username:password' | base64)"
# Get specific customer
curl -X GET \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
-H "Accept: application/json" \
-H "X-API-Key: your-api-key-here"
POST 请求:
curl -X POST \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Basic $(echo -n 'username:password' | base64)" \
-d '{
"name": "Acme Corporation",
"email": "[email protected]",
"phone": "555-1234",
"address": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}
}'
PUT 请求:
curl -X PUT \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Basic $(echo -n 'username:password' | base64)" \
-d '{
"name": "Acme Corporation Updated",
"phone": "555-5678"
}'
DELETE 请求:
curl -X DELETE \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
-H "Accept: application/json" \
-H "Authorization: Basic $(echo -n 'username:password' | base64)"
第 8.3 步:使用 Postman 测试
Postman 集合结构:
{
"info": {
"name": "Customer API",
"description": "ServiceNow Customer Integration API"
},
"variable": [
{ "key": "base_url", "value": "https://dev12345.service-now.com" },
{ "key": "api_path", "value": "/api/x_company/customer_api/v1" }
],
"auth": {
"type": "basic",
"basic": [
{ "key": "username", "value": "{{username}}" },
{ "key": "password", "value": "{{password}}" }
]
},
"item": [
{
"name": "List Customers",
"request": {
"method": "GET",
"url": "{{base_url}}{{api_path}}/customers?limit=10"
}
},
{
"name": "Create Customer",
"request": {
"method": "POST",
"url": "{{base_url}}{{api_path}}/customers",
"body": {
"mode": "raw",
"raw": "{ \"name\": \"Test Customer\", \"email\": \"[email protected]\" }"
}
}
}
]
}
Postman 测试:
// Test response status
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
// Test response structure
pm.test("Response has result array", function () {
var jsonData = pm.response.json();
pm.expect(jsonData).to.have.property('result');
pm.expect(jsonData.result).to.be.an('array');
});
// Test pagination
pm.test("Response has meta with pagination", function () {
var jsonData = pm.response.json();
pm.expect(jsonData.meta).to.have.property('total');
pm.expect(jsonData.meta).to.have.property('limit');
pm.expect(jsonData.meta).to.have.property('offset');
});
// Save sys_id for subsequent requests
pm.test("Save created customer ID", function () {
var jsonData = pm.response.json();
if (jsonData.result && jsonData.result.sys_id) {
pm.environment.set("customer_id", jsonData.result.sys_id);
}
});
第 9 阶段:发布和激活
第 9.1 步:发布前检查清单
发布前,请确认:
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_operation
query: web_service_definition=[api_definition_sys_id]
fields: name,http_method,relative_path,requires_authentication
检查清单:
- 所有操作均已启用身份验证
- 所有脚本均已实现错误处理
- 已配置速率限制
- 已完成输入验证
- 已启用 ACL 强制执行
- 已更新文档
- 测试用例均已通过
- 已捕获更新集
第 9.2 步:发布 API
Tool: SN-Update-Record
Parameters:
table_name: sys_ws_definition
sys_id: [api_definition_sys_id]
data:
published: true
active: true
第 9.3 步:验证发布
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_definition
query: sys_id=[api_definition_sys_id]
fields: name,active,published,service_address
现在可以通过 service_address URL 访问该 API。
工具使用摘要
| 操作 | MCP 工具 | 用途 |
|---|---|---|
| 创建 API | SN-Create-Record (sys_ws_definition) | 创建 API 容器 |
| 创建资源 | SN-Create-Record (sys_ws_operation) | 添加端点 |
| 更新 API | SN-Update-Record | 修改设置 |
| 查询 API | SN-Query-Table | 列出/验证 API |
| 架构发现 | SN-Get-Table-Schema | 了解表结构 |
| 测试脚本 | SN-Execute-Background-Script | 测试 API 逻辑 |
最佳实践
- 先设计: 实施前规划 API 结构
- 从一开始就进行版本控制: 始终启用版本控制(is_versioned: true)
- 保持响应一致: 使用标准错误/成功格式
- 验证所有内容: 处理前验证输入
- 默认安全: 为所有端点启用身份验证
- 完善文档: 使用 short_description 和 doc_link
- 充分测试: 发布前使用 curl/Postman
- 监控使用情况: 记录 API 调用以便调试和收集指标
- 限制速率: 防止滥用
- 使用脚本包含项: 集中管理可复用逻辑
故障排除
API 返回 404
症状: 未找到 API 端点 原因:
- API 未发布/未启用
- URL 路径不正确
- 命名空间错误 解决方案:
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_definition
query: name=[your_api_name]
fields: active,published,service_address,namespace
401 未授权
症状: 身份验证失败 原因:
- 凭据无效
- 用户没有 REST API 访问权限
- 缺少 web_service_admin 角色 解决方案:
- 首先使用 Table API 验证凭据
- 检查用户是否拥有 rest_service 角色
- 如果使用自定义身份验证,请验证 API 密钥
403 禁止访问
症状: 身份验证后访问被拒绝 原因:
- ACL 阻止访问
- 缺少表权限
- 范围应用限制 解决方案:
- 检查底层表上的 ACL
- 确认用户角色符合 ACL 要求
- 检查 API 上的 enforce_acl 设置
500 内部服务器错误
症状: 服务器错误,但没有详细信息 原因:
- 操作中脚本出错
- GlideRecord 查询无效
- 脚本中存在空引用 解决方案:
- 在 System Logs 中检查脚本错误
- 在操作脚本中添加 try-catch
- 先在后台脚本中测试脚本
响应正文为空
症状: 状态为 200,但没有正文 原因:
- 缺少 response.setBody()
- 脚本提前终止
- 在返回前设置正文 解决方案:
- 确认已调用 response.setBody()
- 检查所有代码路径是否都设置了响应
- 确保没有未设置响应就提前返回的情况
脚本未执行
症状: API 返回了响应,但脚本未运行 原因:
- 脚本语法错误
- 操作未关联到 API
- HTTP 方法错误 解决方案:
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_operation
query: web_service_definition=[api_sys_id]
fields: name,http_method,active,operation_script
示例
示例 1:最简 CRUD API
包含基本 CRUD 的完整最简 API:
# 1. Create API Definition
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_definition
data:
name: Simple Task API
namespace: x_company
short_description: Simple task management API
active: true
is_versioned: true
baseline_version: v1
# 2. Create List Operation
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: List Tasks
web_service_definition: [api_sys_id]
http_method: GET
relative_path: /tasks
operation_script: |
(function process(request, response) {
var gr = new GlideRecord('x_company_task');
gr.setLimit(100);
gr.query();
var tasks = [];
while (gr.next()) {
tasks.push({
sys_id: gr.getUniqueValue(),
name: gr.getValue('name'),
status: gr.getValue('status')
});
}
response.setBody({ result: tasks });
})(request, response);
# 3. Create Get Operation
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Get Task
web_service_definition: [api_sys_id]
http_method: GET
relative_path: /tasks/{id}
operation_script: |
(function process(request, response) {
var gr = new GlideRecord('x_company_task');
if (gr.get(request.pathParams.id)) {
response.setBody({
result: {
sys_id: gr.getUniqueValue(),
name: gr.getValue('name'),
description: gr.getValue('description'),
status: gr.getValue('status')
}
});
} else {
response.setStatus(404);
response.setBody({ error: { message: 'Task not found' }});
}
})(request, response);
示例 2:带聚合的搜索 API
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var searchTerm = request.queryParams.q;
var category = request.queryParams.category;
var includeStats = request.queryParams.stats === 'true';
// Search incidents
var gr = new GlideRecord('incident');
if (searchTerm) {
var qc = gr.addQuery('short_description', 'CONTAINS', searchTerm);
qc.addOrCondition('description', 'CONTAINS', searchTerm);
qc.addOrCondition('number', searchTerm);
}
if (category) {
gr.addQuery('category', category);
}
gr.addQuery('active', true);
gr.setLimit(50);
gr.orderByDesc('sys_created_on');
gr.query();
var results = [];
while (gr.next()) {
results.push({
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
priority: gr.getDisplayValue('priority'),
state: gr.getDisplayValue('state'),
created: gr.getValue('sys_created_on')
});
}
var responseBody = {
query: searchTerm,
count: results.length,
results: results
};
// Add statistics if requested
if (includeStats) {
var stats = {};
// Count by priority
var priorityAgg = new GlideAggregate('incident');
priorityAgg.addQuery('active', true);
priorityAgg.addAggregate('COUNT');
priorityAgg.groupBy('priority');
priorityAgg.query();
stats.by_priority = {};
while (priorityAgg.next()) {
var priority = priorityAgg.getDisplayValue('priority');
stats.by_priority[priority] = parseInt(priorityAgg.getAggregate('COUNT'));
}
responseBody.stats = stats;
}
response.setStatus(200);
response.setBody(responseBody);
})(request, response);
示例 3:批量操作 API
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var body = request.body.data;
// Validate bulk request
if (!body.operations || !Array.isArray(body.operations)) {
response.setStatus(400);
response.setBody({
error: {
message: 'operations array required',
code: 'VALIDATION_ERROR'
}
});
return;
}
if (body.operations.length > 100) {
response.setStatus(400);
response.setBody({
error: {
message: 'Maximum 100 operations per request',
code: 'LIMIT_EXCEEDED'
}
});
return;
}
var results = [];
body.operations.forEach(function(op, index) {
var result = { index: index, operation: op.action };
try {
switch (op.action) {
case 'create':
var gr = new GlideRecord('customer');
gr.initialize();
for (var field in op.data) {
gr.setValue(field, op.data[field]);
}
result.sys_id = gr.insert();
result.success = !!result.sys_id;
break;
case 'update':
var grUpdate = new GlideRecord('customer');
if (grUpdate.get(op.sys_id)) {
for (var updateField in op.data) {
grUpdate.setValue(updateField, op.data[updateField]);
}
grUpdate.update();
result.success = true;
} else {
result.success = false;
result.error = 'Record not found';
}
break;
case 'delete':
var grDelete = new GlideRecord('customer');
if (grDelete.get(op.sys_id)) {
grDelete.setValue('active', false);
grDelete.update();
result.success = true;
} else {
result.success = false;
result.error = 'Record not found';
}
break;
default:
result.success = false;
result.error = 'Unknown action: ' + op.action;
}
} catch (e) {
result.success = false;
result.error = e.message;
}
results.push(result);
});
var successCount = results.filter(function(r) { return r.success; }).length;
response.setStatus(successCount === results.length ? 200 : 207); // 207 = Multi-Status
response.setBody({
summary: {
total: results.length,
succeeded: successCount,
failed: results.length - successCount
},
results: results
});
})(request, response);
相关技能
admin/update-set-management- 在更新集中捕获 API 定义admin/application-scope- 在范围应用中创建 APIsecurity/acl-management- 配置 API 安全性admin/script-execution- 测试 API 脚本catalog/request-fulfillment- 由 API 驱动的履行流程