8 Logging & Debugging
When something isn’t working as expected, you need visibility into what bcnav is doing. This tutorial covers bcnav’s logging system and techniques for troubleshooting API issues.
8.1 Enabling logging
bcnav uses Racket’s logging system to report what it’s doing. By default, log messages aren’t displayed. To see them, start a log listener:
Now you’ll see log messages in the REPL as bcnav operates:
> (bc-authenticate!) |
(info) Authenticating with client credentials for tenant: contoso.onmicrosoft.com |
(info) Authentication successful. Token expires at 1704567890 |
#<bc-token> |
8.2 Log levels
bcnav produces messages at different severity levels:
Level | What it includes |
debug | Detailed internal operations, HTTP requests/responses |
info | Normal operations like authentication, cache hits |
warning | Recoverable issues like rate limiting with retry |
error | Failures that stop the operation |
Start the listener at a specific level to control verbosity:
;; Show only info and above (default) (start-bc-listener 'info) ;; Show everything, including debug details (start-bc-listener 'debug) ;; Show only warnings and errors (start-bc-listener 'warning)
8.3 Request logging
For detailed HTTP debugging, enable request logging:
(current-bc-log-requests #t) (start-bc-listener 'debug)
Now you’ll see each HTTP request and response:
> (customers #:query (make-query #:top 2)) |
(debug) GET https://api.businesscentral.dynamics.com/v2.0/.../customers?$top=2 |
(debug) GET https://api.businesscentral.dynamics.com/... -> OK |
'(#hasheq(...) #hasheq(...)) |
Verifying your queries are being constructed correctly
Seeing the actual URLs being called
Diagnosing authentication or network issues
8.4 Stopping the listener
When you’re done debugging, stop the listener:
Or just disable request logging while keeping other messages:
8.5 Common troubleshooting scenarios
8.5.1 Authentication failures
If bc-authenticate! fails, the error message usually explains why:
> (bc-authenticate!) |
OAuth authentication failed |
Status: 400 |
Error: invalid_client |
Description: AADSTS7000218: The request body must contain |
'client_assertion' or 'client_secret'. |
|
Tip: Verify your tenant ID, client ID, and client secret are correct. |
Wrong tenant: Double-check current-bc-tenant
Wrong client ID: Verify against Azure AD app registration
Wrong or expired secret: Secrets expire; check the Azure portal
Missing secret: current-bc-client-secret not set
8.5.2 API errors (4xx responses)
API errors include the status code and response body:
> (customer "nonexistent-id") |
Business Central API Error |
Request: GET https://api.businesscentral.dynamics.com/.../customers(nonexistent-id) |
Status: 404 Not Found |
Response: |
{"error":{"code":"EntityNotFound","message":"The entity was not found."}} |
|
Tip: Check that tenant, environment, and company are correct. |
404 Not Found: Wrong ID, wrong entity name, or wrong company
400 Bad Request: Invalid query syntax or field values
401 Unauthorized: Token expired or insufficient permissions
412 Precondition Failed: ETag mismatch (record was modified)
8.5.3 Rate limiting
BC throttles excessive API requests. bcnav handles this automatically:
> (for ([i 1000]) (customers)) |
(warning) Rate limited, retrying in 1000ms... |
(warning) Rate limited, retrying in 2000ms... |
bcnav retries with exponential backoff (1s, 2s, 4s...) up to a configurable limit. If retries are exhausted, you’ll get an exn:fail:bcnav:rate-limit error.
You can adjust retry behavior:
;; Maximum number of retries (default: 3) (current-bc-max-retries 5) ;; Base delay in milliseconds (default: 1000) (current-bc-retry-base-delay 500)
8.5.4 Checking your configuration
Verify your parameters are set correctly:
> (current-bc-tenant) |
"contoso.onmicrosoft.com" |
|
> (current-bc-environment) |
"Production" |
|
> (current-bc-company) |
"12345678-1234-1234-1234-123456789012" |
|
> (token-valid?) |
#t |
8.6 Debugging queries
If a query isn’t returning expected results, check what bcnav is sending:
> (define q (make-query |
#:filter (_and (_= 'blocked #f) |
(_contains 'displayName "test")) |
#:select '(id displayName) |
#:top 10)) |
|
> (query->params q) |
'(($filter . "(blocked eq false and contains(displayName,'test'))") |
($select . "id,displayName") |
($top . "10")) |
No records match the criteria
Field names are incorrect (case-sensitive!)
Data types don’t match (string vs. boolean vs. number)
8.7 Inspecting responses
When debugging, it helps to look at raw responses. bcnav returns hash tables that you can explore:
> (define cust (customer "some-id")) |
> (hash-keys cust) |
'(id number displayName type email phoneNumber blocked |
@odata.context @odata.etag ...) |
|
> (for ([(k v) (in-hash cust)]) |
(printf "~a: ~v\n" k v)) |
8.8 Using parameterize for testing
Racket’s parameterize lets you temporarily change parameters without affecting global state:
;; Test with a different company (parameterize ([current-bc-company "test-company-id"]) (customers)) ;; Uses test company (customers) ;; Back to original company
Testing against a sandbox without changing your main config
Temporarily enabling verbose logging
Disabling auto-pagination for a single call
;; One-off debug call (parameterize ([current-bc-log-requests #t]) (start-bc-listener 'debug) (customers #:query (make-query #:top 1)))
8.9 Getting help
If you’re stuck:
Check BC’s API documentation for entity-specific requirements
Use inspect to verify field names and types
Enable debug logging to see exactly what’s being sent
Test the same request in a REST client (like Postman) to isolate whether the issue is in bcnav or the API itself