Unsupported OData Features¶
The Danish Parliamentary API (oda.ft.dk) implements OData 3.0 with specific limitations and unsupported features. This page documents what doesn't work, why, and provides alternative approaches.
OData Version Limitations¶
OData 3.0 Only¶
The API strictly implements OData 3.0 and does not support newer OData versions:
# L FAILS: OData 4.0 version headers cause 404 errors
curl -H "OData-Version: 4.0" "https://oda.ft.dk/api/Sag?%24top=1"
# Response: HTTP 404
# WORKS: Default OData 3.0 implementation
curl "https://oda.ft.dk/api/Sag?%24top=1"
# Response: HTTP 200 with DataServiceVersion: 3.0 header
Impact: Modern OData 4.0+ features are unavailable.
Unsupported Query Operations¶
Advanced OData 4.0 Functions¶
These modern query functions are not implemented:
$search - Full-text Search¶
Alternative: Use $filter with substringof():
# WORKS: Text search using OData 3.0 functions
curl "https://oda.ft.dk/api/Sag?%24filter=substringof('klima',titel)&%24top=2"
$apply - Data Aggregation¶
# L FAILS: HTTP 400 Bad Request
curl "https://oda.ft.dk/api/Sag?%24apply=aggregate(id%20with%20count%20as%20total)"
Alternative: Client-side aggregation or multiple filtered queries:
// Count cases by type
const typeA = await fetch('https://oda.ft.dk/api/Sag?$filter=typeid eq 1&$inlinecount=allpages');
const typeB = await fetch('https://oda.ft.dk/api/Sag?$filter=typeid eq 2&$inlinecount=allpages');
const countA = JSON.parse(typeA)['odata.count'];
const countB = JSON.parse(typeB)['odata.count'];
$compute - Computed Properties¶
# L FAILS: Connection timeout/error
curl "https://oda.ft.dk/api/Sag?%24compute=count() as total&%24top=1"
Alternative: Perform calculations client-side after data retrieval.
Batch Operations¶
$batch Endpoint¶
The API does not support batch processing for multiple operations:
# L FAILS: HTTP 404 Not Found
curl "https://oda.ft.dk/api/%24batch"
# L FAILS: POST with multipart content
curl -X POST -H "Content-Type: multipart/mixed; boundary=batch" \
"https://oda.ft.dk/api/%24batch" \
--data-binary "@batch_request.txt"
Impact: - No ability to combine multiple operations in a single request - Higher network overhead for multiple API calls - No transaction support across operations
Alternative: Make individual HTTP requests:
// Instead of batch request, make multiple calls
const [cases, actors, documents] = await Promise.all([
fetch('https://oda.ft.dk/api/Sag?$top=10'),
fetch('https://oda.ft.dk/api/Aktør?$top=10'),
fetch('https://oda.ft.dk/api/Dokument?$top=10')
]);
Write Operations¶
All Modification Methods Rejected¶
Despite HTTP Allow headers advertising write support, all write operations return HTTP 501:
POST (Create)¶
# L HTTP 501 Not Implemented
curl -X POST "https://oda.ft.dk/api/Sag" \
-H "Content-Type: application/json" \
-d '{"titel": "Test case"}'
Response:
{
"odata.error": {
"code": "POST requests are not supported.",
"message": {
"lang": "en-US",
"value": "Creating entities is not supported for this entity set."
}
}
}
PUT (Replace)¶
# L HTTP 501 Not Implemented
curl -X PUT "https://oda.ft.dk/api/Sag(123)" \
-H "Content-Type: application/json" \
-d '{"titel": "Updated case"}'
PATCH (Partial Update)¶
# L HTTP 501 Not Implemented
curl -X PATCH "https://oda.ft.dk/api/Sag(123)" \
-H "Content-Type: application/json" \
-d '{"titel": "Partial update"}'
DELETE (Remove)¶
Why Write Operations Are Blocked¶
- Government Data Integrity: Parliamentary data must remain authentic and unmodified
- Public Access Model: Read-only API ensures data consistency for all users
- Audit Trail Preservation: Historical legislative records cannot be altered
- No Authentication System: Without user authentication, write operations would be insecure
Unsupported HTTP Methods¶
OPTIONS Method¶
Issue: The Allow header still shows unsupported methods:
Reality: Only GET and HEAD methods actually work.
Response Format Limitations¶
XML Format Issues¶
While the API advertises XML support, using $format=xml can cause issues:
# PROBLEMATIC: May return errors in some contexts
curl "https://oda.ft.dk/api/Sag?%24format=xml&%24top=1"
Recommendation: Use the default JSON format for best compatibility.
Content-Type Inconsistencies¶
Error responses have inconsistent content types:
| Status | Error Type | Content-Type | Response Body |
|---|---|---|---|
| 400 | Bad OData syntax | None | Empty |
| 404 | Invalid entity | text/html | HTML error page |
| 404 | Invalid ID | None | Empty |
| 501 | Write operations | application/json | Structured error |
Missing Monitoring Endpoints¶
Standard production monitoring endpoints are not available:
# L All return HTTP 404
curl "https://oda.ft.dk/health"
curl "https://oda.ft.dk/api/health"
curl "https://oda.ft.dk/ping"
curl "https://oda.ft.dk/api/status"
curl "https://oda.ft.dk/robots.txt"
Alternative: Use entity queries to check API health:
// Health check using actual API endpoint
async function checkAPIHealth() {
try {
const response = await fetch('https://oda.ft.dk/api/Sag?$top=1');
return response.status === 200;
} catch (error) {
return false;
}
}
No Real-time Capabilities¶
Webhook Support¶
The API does not support push notifications or webhooks:
# L All return HTTP 404
curl "https://oda.ft.dk/webhook"
curl "https://oda.ft.dk/api/notifications"
curl "https://oda.ft.dk/api/subscribe"
curl "https://oda.ft.dk/rss"
Alternative: Implement polling strategies:
// Poll for recent updates
async function pollForUpdates(entity, lastCheckTime) {
const isoDate = lastCheckTime.toISOString().slice(0, 19);
const response = await fetch(
`https://oda.ft.dk/api/${entity}?$filter=opdateringsdato gt datetime'${isoDate}'&$orderby=opdateringsdato desc`
);
return response.json();
}
// Check every 10 minutes
setInterval(() => {
pollForUpdates('Sag', new Date(Date.now() - 10 * 60 * 1000));
}, 10 * 60 * 1000);
Query Limitations¶
Maximum $top Value¶
The API enforces a maximum of 100 records per request:
# L Silently limited to 100 records
curl "https://oda.ft.dk/api/Sag?%24top=1000" | jq '.value | length'
# Returns: 100
# Explicit 100 limit works
curl "https://oda.ft.dk/api/Sag?%24top=100" | jq '.value | length'
# Returns: 100
# Values under 100 work correctly
curl "https://oda.ft.dk/api/Sag?%24top=50" | jq '.value | length'
# Returns: 50
Workaround: Use pagination with $skip:
// Get large datasets through pagination
async function getAllRecords(entity, pageSize = 100) {
let allRecords = [];
let skip = 0;
while (true) {
const response = await fetch(
`https://oda.ft.dk/api/${entity}?$top=${pageSize}&$skip=${skip}`
);
const data = await response.json();
if (data.value.length === 0) break;
allRecords.push(...data.value);
skip += pageSize;
}
return allRecords;
}
Error Handling Gaps¶
Silent Filter Failures¶
Critical Issue: Invalid filter field names are silently ignored and return the complete unfiltered dataset:
# L DANGEROUS: Returns ALL records instead of error
curl "https://oda.ft.dk/api/Sag?%24filter=invalid_field_name eq 'test'"
# Returns: 100 records (unfiltered)
Detection Strategy:
// Check for unexpectedly large results
function validateFilterResults(data, expectedSubset = true) {
if (expectedSubset && data.value.length === 100) {
console.warn('Received 100 results - filter may have been ignored');
console.warn('Verify filter field names in $metadata');
}
}
Empty Error Responses¶
HTTP 400 errors return empty response bodies with no error details:
# L Empty response body for bad syntax
curl "https://oda.ft.dk/api/Sag?%24expand=InvalidRelationship"
# Response: HTTP 400 with 0 bytes content
Client-Side Error Handling:
async function makeAPIRequest(url) {
const response = await fetch(url);
switch(response.status) {
case 400:
throw new Error(`Bad Request: Check OData syntax in ${url}`);
case 404:
const contentType = response.headers.get('content-type');
if (contentType?.includes('text/html')) {
throw new Error(`Entity endpoint not found`);
} else {
throw new Error(`Entity ID not found`);
}
case 501:
const errorBody = await response.json();
throw new Error(`Operation not supported: ${errorBody.odata.error.message.value}`);
default:
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
return response.json();
}
Best Practices for Working with Limitations¶
1. Always Use URL Encoding¶
# CORRECT: Use %24 instead of $
curl "https://oda.ft.dk/api/Sag?%24top=10"
# L WRONG: Literal $ characters may cause issues
curl "https://oda.ft.dk/api/Sag?\$top=10"
2. Implement Client-Side Validation¶
// Validate filter field names against $metadata
async function validateFilter(entity, filterFields) {
const metadataResponse = await fetch('https://oda.ft.dk/api/$metadata');
const metadataXML = await metadataResponse.text();
// Extract field names for entity (simplified example)
const entityPattern = new RegExp(`EntityType Name="${entity}"[\\s\\S]*?</EntityType>`);
const entityMatch = metadataXML.match(entityPattern);
if (entityMatch) {
const validFields = entityMatch[0].match(/Property Name="([^"]+)"/g) || [];
const validFieldNames = validFields.map(f => f.match(/Name="([^"]+)"/)[1]);
// Check if filter fields exist
for (const field of filterFields) {
if (!validFieldNames.includes(field)) {
console.warn(`Warning: Field '${field}' not found in ${entity} entity`);
}
}
}
}
3. Handle Large Datasets Efficiently¶
// Stream processing for large datasets
async function* paginateAll(entity, options = {}) {
let skip = 0;
const top = Math.min(options.top || 100, 100);
while (true) {
const url = new URL(`https://oda.ft.dk/api/${entity}`);
url.searchParams.set('$top', top.toString());
url.searchParams.set('$skip', skip.toString());
if (options.filter) url.searchParams.set('$filter', options.filter);
if (options.select) url.searchParams.set('$select', options.select);
if (options.expand) url.searchParams.set('$expand', options.expand);
const response = await fetch(url.toString());
const data = await response.json();
if (data.value.length === 0) break;
yield* data.value;
skip += top;
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
}
}
Future Considerations¶
The Danish Parliamentary API is a production government system focused on stability and data integrity. Future enhancements are unlikely to include:
- Write operations (data integrity requirements)
- OData 4.0 upgrade (backward compatibility concerns)
- Real-time push notifications (polling architecture is sufficient)
- Batch operations (current individual request model works well)
For feature requests or API issues, contact: folketinget@ft.dk with subject "Åbne Data".
Summary¶
The Danish Parliamentary API provides comprehensive read-only access to parliamentary data with specific limitations:
- OData 3.0 only - no modern 4.0+ features
- Read-only access - all write operations return HTTP 501
- 100 record limit per request - use pagination for larger datasets
- Silent filter failures - invalid field names return unfiltered data
- No batch processing - make individual requests
- No real-time updates - polling required
Despite these limitations, the API provides excellent access to 30+ years of Danish parliamentary data with strong performance and reliability.