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

  1. Check Service.is_custom before calling and route curated apps to updateCustomApp
  2. If the app should be custom, fix the service record's is_custom flag
  3. 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

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


AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27). Data as JSON: /api/errors/8e558178f1312fbe. Report an issue: GitHub.