bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
479 lines (406 loc) • 17.6 kB
Markdown
# Business Central API Error Response Patterns - AL Code Sample
## Basic API Error Handling
```al
page 50120 "Customer API Error Handling"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'customers';
APIVersion = 'v1.0';
EntityName = 'customer';
EntitySetName = 'customers';
SourceTable = Customer;
DelayedInsert = true;
layout
{
area(Content)
{
repeater(Records)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
}
field(number; Rec."No.")
{
Caption = 'Number';
trigger OnValidate()
begin
// Validation error - returns 400 Bad Request
if Rec."No." = '' then
Error('Customer number cannot be empty');
if StrLen(Rec."No.") > 20 then
Error('Customer number cannot exceed 20 characters');
// Check for duplicates - returns 400 Bad Request
if CustomerExists(Rec."No.") then
Error('Customer with number %1 already exists', Rec."No.");
end;
}
field(name; Rec.Name)
{
Caption = 'Name';
trigger OnValidate()
begin
// Required field validation
if Rec.Name = '' then
Error('Customer name is required');
end;
}
field(creditLimit; Rec."Credit Limit (LCY)")
{
Caption = 'Credit Limit';
trigger OnValidate()
begin
// Business logic validation
if Rec."Credit Limit (LCY)" < 0 then
Error('Credit limit cannot be negative');
// Custom business rule
if (Rec."Credit Limit (LCY)" > 100000) and (Rec."Customer Posting Group" <> 'WHOLESALE') then
Error('Credit limit above 100,000 requires wholesale customer posting group');
end;
}
field(blocked; Rec.Blocked)
{
Caption = 'Blocked';
}
}
}
}
// Custom validation procedures
local procedure CustomerExists(CustomerNo: Code[20]): Boolean
var
Customer: Record Customer;
begin
Customer.SetRange("No.", CustomerNo);
exit(not Customer.IsEmpty);
end;
}
```
## Advanced Error Handling with Custom Messages
```al
codeunit 50120 "API Error Management"
{
// Helper procedures for consistent API error handling
procedure ValidateCustomerForAPI(var Customer: Record Customer)
begin
// Comprehensive customer validation
ValidateRequiredFields(Customer);
ValidateBusinessRules(Customer);
ValidateDataIntegrity(Customer);
end;
local procedure ValidateRequiredFields(var Customer: Record Customer)
begin
if Customer."No." = '' then
ThrowValidationError('MISSING_CUSTOMER_NUMBER', 'Customer number is required');
if Customer.Name = '' then
ThrowValidationError('MISSING_CUSTOMER_NAME', 'Customer name is required');
if Customer."Customer Posting Group" = '' then
ThrowValidationError('MISSING_POSTING_GROUP', 'Customer posting group is required');
end;
local procedure ValidateBusinessRules(var Customer: Record Customer)
var
CustomerPostingGroup: Record "Customer Posting Group";
PaymentTerms: Record "Payment Terms";
begin
// Validate posting group exists
if not CustomerPostingGroup.Get(Customer."Customer Posting Group") then
ThrowValidationError('INVALID_POSTING_GROUP',
StrSubstNo('Customer posting group %1 does not exist', Customer."Customer Posting Group"));
// Validate payment terms if specified
if Customer."Payment Terms Code" <> '' then
if not PaymentTerms.Get(Customer."Payment Terms Code") then
ThrowValidationError('INVALID_PAYMENT_TERMS',
StrSubstNo('Payment terms %1 do not exist', Customer."Payment Terms Code"));
// Credit limit business rules
if Customer."Credit Limit (LCY)" > 1000000 then
ThrowValidationError('CREDIT_LIMIT_EXCEEDED',
'Credit limit cannot exceed 1,000,000 without special approval');
end;
local procedure ValidateDataIntegrity(var Customer: Record Customer)
var
ExistingCustomer: Record Customer;
begin
// Check for duplicate customer names in same posting group
ExistingCustomer.SetRange("Customer Posting Group", Customer."Customer Posting Group");
ExistingCustomer.SetRange(Name, Customer.Name);
ExistingCustomer.SetFilter("No.", '<>%1', Customer."No.");
if not ExistingCustomer.IsEmpty then
ThrowValidationError('DUPLICATE_CUSTOMER_NAME',
StrSubstNo('Customer with name %1 already exists in posting group %2',
Customer.Name, Customer."Customer Posting Group"));
end;
local procedure ThrowValidationError(ErrorCode: Text; ErrorMessage: Text)
begin
// This creates a structured error response
// BC automatically wraps this in proper OData error format
Error('API_VALIDATION_ERROR:%1:%2', ErrorCode, ErrorMessage);
end;
}
// Usage in API page
page 50121 "Customer API Enhanced"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'customers';
APIVersion = 'v2.0';
EntityName = 'customer';
EntitySetName = 'customers';
SourceTable = Customer;
DelayedInsert = true;
layout
{
area(Content)
{
repeater(Records)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
}
field(number; Rec."No.")
{
Caption = 'Number';
}
field(name; Rec.Name)
{
Caption = 'Name';
}
field(customerPostingGroup; Rec."Customer Posting Group")
{
Caption = 'Customer Posting Group';
}
field(creditLimit; Rec."Credit Limit (LCY)")
{
Caption = 'Credit Limit';
}
}
}
}
var
APIErrorMgt: Codeunit "API Error Management";
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
// Validate before insert - returns 400 if validation fails
APIErrorMgt.ValidateCustomerForAPI(Rec);
exit(true);
end;
trigger OnModifyRecord(): Boolean
begin
// Validate before modify - returns 400 if validation fails
APIErrorMgt.ValidateCustomerForAPI(Rec);
exit(true);
end;
trigger OnDeleteRecord(): Boolean
begin
// Check if customer can be deleted - returns 409 if conflict
CheckCustomerCanBeDeleted();
exit(true);
end;
local procedure CheckCustomerCanBeDeleted()
var
CustLedgerEntry: Record "Cust. Ledger Entry";
SalesHeader: Record "Sales Header";
begin
// Check for open transactions
CustLedgerEntry.SetRange("Customer No.", Rec."No.");
CustLedgerEntry.SetRange(Open, true);
if not CustLedgerEntry.IsEmpty then
Error('CUSTOMER_HAS_OPEN_ENTRIES:Cannot delete customer with open ledger entries');
// Check for pending orders
SalesHeader.SetRange("Sell-to Customer No.", Rec."No.");
SalesHeader.SetFilter("Document Type", '%1|%2', SalesHeader."Document Type"::Order, SalesHeader."Document Type"::Invoice);
if not SalesHeader.IsEmpty then
Error('CUSTOMER_HAS_PENDING_ORDERS:Cannot delete customer with pending sales orders');
end;
}
```
## HTTP Status Code Mapping and Error Categories
```al
codeunit 50121 "API Error Response Handler"
{
// Demonstrates how different AL errors map to HTTP status codes
procedure DemonstrateErrorTypes(var Customer: Record Customer)
begin
// 400 Bad Request - Validation errors
ValidateRequiredData(Customer);
// 404 Not Found - Record not found
ValidateRecordExists(Customer);
// 409 Conflict - Business rule conflicts
ValidateBusinessConflicts(Customer);
// 422 Unprocessable Entity - Semantic errors
ValidateSemanticRules(Customer);
end;
local procedure ValidateRequiredData(var Customer: Record Customer)
begin
// These errors result in 400 Bad Request
if Customer."No." = '' then
Error('Customer number is required'); // 400
if Customer.Name = '' then
Error('Customer name cannot be empty'); // 400
if StrLen(Customer."No.") > 20 then
Error('Customer number exceeds maximum length of 20 characters'); // 400
end;
local procedure ValidateRecordExists(var Customer: Record Customer)
var
CustomerPostingGroup: Record "Customer Posting Group";
PaymentTerms: Record "Payment Terms";
begin
// These errors result in 400 Bad Request (not 404 - BC limitation)
if Customer."Customer Posting Group" <> '' then
if not CustomerPostingGroup.Get(Customer."Customer Posting Group") then
Error('Customer posting group %1 does not exist', Customer."Customer Posting Group");
if Customer."Payment Terms Code" <> '' then
if not PaymentTerms.Get(Customer."Payment Terms Code") then
Error('Payment terms %1 do not exist', Customer."Payment Terms Code");
end;
local procedure ValidateBusinessConflicts(var Customer: Record Customer)
var
ExistingCustomer: Record Customer;
begin
// These errors typically result in 400 Bad Request
// BC doesn't directly support 409 Conflict, but these are conceptually conflicts
ExistingCustomer.SetRange("No.", Customer."No.");
ExistingCustomer.SetFilter(SystemId, '<>%1', Customer.SystemId);
if not ExistingCustomer.IsEmpty then
Error('Customer number %1 already exists', Customer."No."); // Conceptually 409
end;
local procedure ValidateSemanticRules(var Customer: Record Customer)
begin
// Complex business rule validation - 400 Bad Request
if (Customer."Credit Limit (LCY)" > 50000) and (Customer."Payment Terms Code" = '') then
Error('High credit limit customers must have payment terms specified');
if Customer.Blocked <> Customer.Blocked::" " then
if Customer."Credit Limit (LCY)" > 0 then
Error('Blocked customers cannot have credit limit greater than zero');
end;
// Error formatting for consistent API responses
procedure FormatAPIError(ErrorCode: Text; ErrorMessage: Text; ErrorCategory: Text): Text
var
ErrorJsonObject: JsonObject;
ErrorJson: Text;
begin
// Create structured error response
ErrorJsonObject.Add('code', ErrorCode);
ErrorJsonObject.Add('message', ErrorMessage);
ErrorJsonObject.Add('category', ErrorCategory);
ErrorJsonObject.Add('timestamp', CurrentDateTime);
ErrorJsonObject.WriteTo(ErrorJson);
exit(ErrorJson);
end;
}
```
## Error Response Examples and Testing
```al
codeunit 50122 "API Error Testing"
{
// Test various error scenarios to understand response formats
[Test]
procedure TestValidationErrors()
var
Customer: Record Customer;
APIErrorHandler: Codeunit "API Error Response Handler";
begin
// Test required field validation
Customer.Init();
Customer."No." := ''; // Missing required field
asserterror APIErrorHandler.DemonstrateErrorTypes(Customer);
// Expected response: 400 Bad Request
// {
// "error": {
// "code": "BadRequest",
// "message": "Customer number is required"
// }
// }
Assert.ExpectedError('Customer number is required');
end;
[Test]
procedure TestBusinessRuleViolation()
var
Customer: Record Customer;
APIErrorHandler: Codeunit "API Error Response Handler";
begin
// Test business rule validation
Customer.Init();
Customer."No." := 'TEST001';
Customer.Name := 'Test Customer';
Customer."Credit Limit (LCY)" := 75000;
Customer."Payment Terms Code" := ''; // Violates business rule
asserterror APIErrorHandler.DemonstrateErrorTypes(Customer);
// Expected response: 400 Bad Request
// {
// "error": {
// "code": "BadRequest",
// "message": "High credit limit customers must have payment terms specified"
// }
// }
Assert.ExpectedError('High credit limit customers must have payment terms specified');
end;
[Test]
procedure TestRecordNotFound()
var
Customer: Record Customer;
APIErrorHandler: Codeunit "API Error Response Handler";
begin
// Test reference to non-existent record
Customer.Init();
Customer."No." := 'TEST002';
Customer.Name := 'Test Customer';
Customer."Customer Posting Group" := 'NONEXISTENT';
asserterror APIErrorHandler.DemonstrateErrorTypes(Customer);
// Expected response: 400 Bad Request (BC limitation - should be 404)
// {
// "error": {
// "code": "BadRequest",
// "message": "Customer posting group NONEXISTENT does not exist"
// }
// }
Assert.ExpectedError('Customer posting group NONEXISTENT does not exist');
end;
}
// Error response interceptor for custom formatting
codeunit 50123 "API Error Formatter"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"API Error Publisher", 'OnAfterPublishError', '', false, false)]
local procedure OnAfterPublishAPIError(ErrorContext: Text; var ErrorPayload: Text)
var
ErrorDetails: JsonObject;
CustomError: JsonObject;
begin
// Intercept and format API errors for consistent response structure
if ErrorDetails.ReadFrom(ErrorPayload) then begin
CustomError.Add('timestamp', CurrentDateTime);
CustomError.Add('path', ErrorContext);
CustomError.Add('error', ErrorDetails);
CustomError.Add('apiVersion', 'v2.0');
CustomError.WriteTo(ErrorPayload);
end;
end;
}
```
## Implementation Notes
**BC API Error Response Behavior:**
- Most AL errors return 400 Bad Request status code
- BC automatically wraps AL Error() calls in OData error format
- Custom error codes can be embedded in error messages
- Error details include message, code, and sometimes inner error information
**Error Categories and HTTP Status Mapping:**
- **400 Bad Request**: Validation errors, missing required fields, format errors
- **401 Unauthorized**: Authentication failures (handled by BC framework)
- **403 Forbidden**: Permission denied (handled by BC framework)
- **404 Not Found**: Invalid API endpoint (handled by BC framework)
- **409 Conflict**: Not directly supported - use 400 with descriptive message
- **422 Unprocessable Entity**: Not directly supported - use 400
- **500 Internal Server Error**: Unexpected AL runtime errors
**Best Practices:**
- Use descriptive error messages that help API consumers
- Include error codes for programmatic error handling
- Validate data comprehensively before database operations
- Handle business rule violations gracefully
- Test error scenarios thoroughly
- Consider creating custom error response formats for better API experience
**Testing Error Responses:**
- Use BC Test Toolkit to validate error handling
- Test all validation scenarios systematically
- Verify error message clarity and usefulness
- Ensure consistent error format across API endpoints
- Monitor error logs for unexpected error patterns