Crosstalk-Solutions/project-nomad · warning
Only custom apps can be updated this way.
Error message
Only custom apps can be updated this way.
What it means
HTTP 403 returned by the admin API when updateCustomApp_pullLatest is called for a service whose is_custom flag is false. The endpoint only recreates containers of user-created custom apps; curated or dependency services must be edited via updateCustomApp instead.
Source
Thrown at admin/app/controllers/system_controller.ts:541
})
}
service.custom_url = normalized
await service.save()
return response.send({ success: true, custom_url: service.custom_url })
}
/** Re-pull a custom app's image and recreate its container in place (preserving volumes). */
async updateCustomApp_pullLatest({ request, response }: HttpContext) {
const payload = await request.validateUsing(installServiceValidator)
const service = await Service.query().where('service_name', payload.service_name).first()
if (!service) {
return response.status(404).send({ success: false, message: `Service ${payload.service_name} not found` })
}
if (!service.is_custom) {
return response.status(403).send({ success: false, message: 'Only custom apps can be updated this way.' })
}
const result = await this.dockerService.recreateCustomAppContainer(payload.service_name, {
forcePull: true,
})
if (result.success) {
return response.send({ success: true, message: result.message })
}
return response.status(400).send({ success: false, message: result.message })
}
/** Return the last N lines of a service container's logs. */
async getServiceLogs({ params, request, response }: HttpContext) {
// Scope to managed services only — otherwise any sibling container's logs (admin app,
// database) would be readable by name on this unauthenticated API surface.
const service = await Service.query().where('service_name', params.name).first()
if (!service) {
return response.status(404).send({ success: false, message: `Service ${params.name} not found` })View on GitHub (pinned to 0bd1c6f4f9)
Solutions
- Check Service.is_custom before calling and route curated apps to updateCustomApp
- If the app should be custom, fix the service record's is_custom flag
- Ensure the UI disables 'pull latest' for non-custom apps
Example fix
// before
await fetch(`/api/custom-apps/${name}/pull-latest`, { method: 'POST' })
// after
if (app.is_custom) {
await fetch(`/api/custom-apps/${name}/pull-latest`, { method: 'POST' })
} else {
await fetch('/api/custom-apps', { method: 'PUT', body: JSON.stringify(app) })
} Defensive patterns
Strategy: validation
Validate before calling
const svc = await api.getService(name)
if (!svc?.is_custom) throw new Error('Use updateCustomApp for non-custom apps') Type guard
const isCustomApp = (s: Service | null): s is Service & { is_custom: true } => !!s?.is_custom Prevention
- Check is_custom before choosing endpoint
- Route curated apps to updateCustomApp
When it happens
Trigger: POST to the custom-app pull-latest endpoint with a service_name that exists in the services table but was seeded/curated (is_custom = false), e.g. pulling latest for a pre-configured app.
Common situations: Calling the wrong update endpoint for a curated app; frontend routing both custom and curated apps to the pull-latest action; service rows seeded without the custom flag.
Related errors
- sysbench disk-write benchmark produced no parseable MiB/s —
- Sysbench command failed: ${error.message}
- Failed to get auth token from ${registry}: ${response.status
- No token returned from ${registry}
- recreated container ${readiness.reason}
AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27).
Data as JSON: /api/errors/8e558178f1312fbe.
Report an issue: GitHub.