Errors — glance — branch stable/2026.2
Branch
stable/2026.2@a3e5f490b7a2— repository https://opendev.org/openstack/glance — analysed on 2026-10-28
Glance's exceptions (glance/common/exception.py, 86 classes) carry no HTTP status: GlanceException only formats message with its keyword arguments (glance/common/exception.py:31). Each handler converts the exceptions it expects into webob.exc.HTTP* (287 conversions found in the code), and anything left unconverted becomes a 500 in wsgi.Resource (glance/common/wsgi.py:516-522). There is no business error code: the client only gets the status and an explanation text (see Error body).
Error body
Glance returns webob's default error page, negotiated on the Accept header of the request (checked with the webob of the analysis venv and in the recorded exchanges, _rewrite/exchanges_stable-2026.2.json):
Accept | Content-Type | Body |
|---|---|---|
| absent | text/plain; charset=UTF-8 | 404 Not Found\n\nImage x not found.\n\n |
application/json | application/json | {"message": "Image x not found.<br /><br />\n\n\n", "code": "404 Not Found", "title": "Not Found"} |
text/html, */* | text/html | <html><head><title>404 Not Found</title></head><body><h1>404 Not Found</h1>Image x not found.<br /><br />…</body></html> |
- The explanation is translated to the request's
Accept-Languagewhen a catalog exists (glance/common/wsgi.py:442-459). - There is no error code and no structured field: clients that need the reason parse the text (see 07 § 4).
- In the JSON form,
messagekeeps webob's HTML line breaks (<br /><br />) and trailing newlines. - Every error response also carries
x-openstack-request-id(glance/api/middleware/context.py:72-85). - A few handlers return 204 with an empty body where an error could be expected (download of an image whose data disappeared,
glance/api/v2/image_data.py:678-679) or a 405 with a custom body template (DELETE /v2/tasks/{id},glance/api/v2/tasks.py:158-165).
Exception → HTTP translation
- Where: in each controller (
glance/api/v2/*.py) and in two cross-cutting layers:- the v2 policy layer (
glance/api/v2/policy.py) raisesHTTPForbiddendirectly, and turns it into 404 when the caller cannot see the image or metadef namespace (glance/api/v2/policy.py:173-193,glance/api/v2/policy.py:335-347): a refused action on an invisible image looks exactly like a missing image; - the notifier layer (
glance/notifier.py:436-503) catches store errors raised duringset_data, emits animage.uploadERROR notification and re-raises them aswebob.exc.HTTP*, so that upload errors keep a defined status whatever the caller.
- the v2 policy layer (
- What falls through to 500: any exception not listed by the handler.
wsgi.ResourcelogsCaught error: …with the traceback and returns a bareHTTPInternalServerError(glance/common/wsgi.py:516-522). Known cases: an unexpected store exception ondelete_from_storeand on location removal is turned into 500 on purpose, with the store message as explanation (glance/api/v2/images.py:820-822,glance/api/v2/images.py:1156-1159);SessionInitErrorof oslo.limit (misconfigured unified limits) is re-raised and becomes 500 (glance/quota/keystone.py:65-73). - Same exception, different statuses (traps for clients):
| Exception | Status | Where |
|---|---|---|
InvalidImageStatusTransition | 400 | DELETE /v2/images/{id} (glance/api/v2/images.py:958-959), deactivate/reactivate (glance/api/v2/image_actions.py:72-73) |
InvalidImageStatusTransition | 409 | upload, stage, import (glance/api/v2/image_data.py:238-240, glance/api/v2/images.py:503-504) |
exception.Invalid | 404 | DELETE /v2/stores/{store}/{image} when the image has no location in that store (glance/api/v2/images.py:799-805) |
exception.Invalid | 400 | everywhere else |
exception.Forbidden | 404 | image members show (glance/api/v2/image_members.py:305-312), metadef reads, image actions on invisible images (policy layer) |
exception.NotFound / Forbidden | 404 without explanation | GET /v2/images/{id}/tasks (glance/api/v2/images.py:608-609) |
glance_store.NotFound | 204 (empty body) | GET /v2/images/{id}/file when the data is gone from the store (glance/api/v2/image_data.py:678-679) |
glance_store.NotFound, ImageNotFound, Conflict | 410 | upload when the image was deleted during the upload (glance/api/v2/image_data.py:185-201) |
Catalogue
One row per situation. "Client sees" is the status after translation; 500 means not translated.
Images and image data
| Exception | Message (template) | Situation | Raised at | Client sees |
|---|---|---|---|---|
ImageNotFound (NotFound) | "Image with the given id %(image_id)s was not found" | the image does not exist, is deleted, or is not visible to the caller | glance/db/__init__.py:73-76 | 404 |
Forbidden | "You are not authorized to complete %(action)s action." | the image is not mutable by the caller (owner check in the DB layer) | glance/db/sqlalchemy/api.py:135-146 | 403, or 404 if the image is invisible |
ProtectedImageDelete (Forbidden) | "Image %(image_id)s is protected and cannot be deleted." | DELETE of an image with protected = true | glance/domain/__init__.py:251 | 403 |
InvalidImageStatusTransition (Invalid) | "Image status transition from %(cur_status)s to %(new_status)s is not allowed" | the image state machine refuses the change, e.g. upload to an active image, delete of a deleted image | glance/domain/__init__.py:155 | 409 (data, import) or 400 (delete, actions) |
ValueError "Properties %s must be set prior to saving data." | — | upload, stage or import without disk_format and/or container_format | glance/domain/__init__.py:159-170 | 400 (upload: "…"; import checks it first with 409 "'container_format' needs to be set before import") |
Forbidden "Not allowed to deactivate image in status '%s'" | — | deactivate an image that is not active/deactivated | glance/domain/__init__.py:257-268 | 403 |
Forbidden "Not allowed to reactivate image in status '%s'" | — | reactivate an image that is not deactivated/active | glance/domain/__init__.py:270-281 | 403 |
Forbidden "The requested image has been deactivated. Image data download is forbidden." | — | download of a deactivated image by a non-admin | glance/api/v2/image_data.py:483-487 | 403 |
ReadonlyProperty (Forbidden) | "Attribute '%(property)s' is read-only." | create with a read-only attribute (status, size, checksum…) | glance/domain/__init__.py:58 | 403 |
ReservedProperty (Forbidden) | "Attribute '%(property)s' is reserved." | create or PATCH with location, deleted, deleted_at or an os_glance_* property; property protections | glance/domain/__init__.py:65-69, glance/api/property_protections.py:41 | 403 |
DuplicateLocation (Duplicate) | "The location %(location)s already exists" | the same URL twice in locations | glance/location.py:230, glance/location.py:273 | 400 |
BadStoreUri | "The Store URI was malformed." | a location URL that no configured store accepts, or that the store cannot reach | glance/location.py:148, glance/location.py:164 | 400 |
InvalidImageData (Invalid) | "Image data is not acceptable or does not match parameters" | the uploaded bytes do not match disk_format, or fail the GPT/MBR safety checks, or format inspection failed with require_image_format_match | glance/location.py:612, glance/location.py:665, glance/location.py:688, glance/location.py:693 | 415 |
cursive SignatureVerificationError | "Signature verification failed" | img_signature* properties present and the data does not match the signature | glance/location.py:556 | 400 |
UploadException | "Image upload problem: %s" | the store returned a size or hash different from the value already recorded on the image | glance/location.py:572 | 500 [inferred: not caught by the upload handler] |
StorageQuotaFull | "The size of the data %(image_size)s will exceed the limit. %(remaining)s bytes remaining." | legacy user_storage_quota exceeded, before or after the write | glance/api/common.py:187, glance/api/common.py:196 | 413 |
ImageSizeLimitExceeded | "The provided image is too large." | more than image_size_cap bytes (default 1 TiB) | glance/db/__init__.py:183, glance/db/__init__.py:197, glance/common/utils.py:467 | 413 |
LimitExceeded | (oslo.limit message) | unified limits: image_size_total, image_stage_total, image_count_total or image_count_uploading exceeded | glance/quota/keystone.py:64 | 413 |
ImagePropertyLimitExceeded (LimitExceeded) | "The limit has been exceeded on the number of allowed image properties. Attempted: %(attempted)s, Maximum: %(maximum)s" | more than image_property_quota (128) properties | glance/quota/__init__.py:113 | 413 |
ImageTagLimitExceeded | "…number of allowed image tags…" | more than image_tag_quota (128) tags | glance/quota/__init__.py:46 | 413 |
ImageLocationLimitExceeded (LimitExceeded) | "…number of allowed image locations…" | more than image_location_quota (10) locations | glance/quota/__init__.py:86 | 413 |
ImageMemberLimitExceeded | "…number of allowed image members for this image…" | more than image_member_quota (128) members | glance/quota/__init__.py:204 | 413 |
Conflict "Image has active task" | — | another import or add-location holds the image lock and it has not expired | glance/api/v2/images.py:220 | 409 |
Conflict "New operation on image '%s' is not permitted as prior operation is still in progress" | — | lost the race to take the import lock | glance/api/v2/images.py:481-484, glance/api/v2/images.py:1226-1230 | 409 |
Conflict (import preconditions) | "Image with status active cannot be target for import", "Only images with status active can be targeted for copying", "Image needs to be in 'queued' state to use '%s' method", "Image needs to be staged before 'glance-direct' method can be used", "'container_format' needs to be set before import", "'disk_format' needs to be set before import" | POST /v2/images/{id}/import on an image in the wrong state | glance/api/v2/images.py:335-357 | 409 |
Store errors (glance_store)
| Store exception | Situation | Client sees | Translated at |
|---|---|---|---|
StorageFull | the backend has no space left | 413 | glance/api/v2/image_data.py:251-257 |
StorageWriteDenied | the backend refused the write | 503 | glance/api/v2/image_data.py:287-293 |
StoreAddDisabled | the store is read-only (http) | 410 | glance/api/v2/image_data.py:230-236 |
Duplicate | data already present for this image id (staging) | 409 "The image %s has data on staging" | glance/api/v2/image_data.py:406-408 |
TimeoutError | filesystem store operation timed out | 503 | glance/api/v2/images.py:125-128, glance/api/v2/image_data.py:294-299 |
NotFound | download: no data at the location | 204 empty body | glance/api/v2/image_data.py:678-679 |
RemoteServiceUnavailable | download: the remote store (HTTP location, Swift…) is unreachable | 503 | glance/api/v2/image_data.py:680-681 |
StoreGetNotSupported, StoreRandomGetNotSupported | download or Range request not supported by the store | 400 | glance/api/v2/image_data.py:682-684 |
InUseByStore, HasSnapshot | delete: the data is in use (e.g. RBD clones) | 409 | glance/api/v2/images.py:943-951 |
UnknownScheme | upload or import names a store that does not exist | 400 (upload), 409 (import) | glance/api/v2/image_data.py:124-127, glance/api/v2/images.py:398-400 |
Request validation (deserializers)
| Message | Situation | Client sees | Raised at |
|---|---|---|---|
| "Malformed JSON in request body." / "Unexpected body type. Expected list/dict." | body is not JSON, or not an object/array | 400 | glance/common/wsgi.py:404-413 |
| "Body expected in request." | POST/PATCH without body | 400 | glance/api/v2/images.py:1313-1318 |
| "A body is not expected with this request." | body sent to a route marked body_reject (GET, DELETE…) | 400 | glance/common/wsgi.py:500-502 |
| "Attribute '%s' is read-only." | create with direct_url, self, file, schema, stores | 403 | glance/api/v2/images.py:1321-1325 |
| "Custom property should not be greater than 255 characters." | property name longer than 255 | 400 | glance/api/v2/images.py:1351-1355 |
JSON schema error text (InvalidObject) | the body does not match /v2/schemas/image, member, task, metadef schemas | 400 | glance/schema.py:38 |
| (Accept-Patch header) | PATCH with a content type other than application/openstack-images-v2.0-json-patch or …-v2.1-json-patch | 415 with Accept-Patch | glance/api/v2/images.py:1509-1521 |
| "Invalid sort key: …", "Invalid sort direction: …", "limit param must be an integer", "limit param must be positive", "Invalid status: …", "Invalid visibility value: …", "The \"changes-since\" filter is no longer available on v2." | bad list parameters | 400 | glance/api/v2/images.py:1552-1599 |
| "Invalid value '%s' for 'os_hidden' filter." / "'protected' filter" | filter values other than true/false | 400 | glance/api/v2/images.py:529-541 |
| (unsupported media type) | upload or stage with a content type other than application/octet-stream | 415 | glance/api/v2/image_data.py:572-575, glance/api/v2/image_data.py:587-590 |
| "Invalid or missing image size in request headers." | non-integer x-openstack-image-size | 400 | glance/api/v2/image_data.py:557-569 |
| "'glance-direct' method is not available at this site." | stage while glance-direct is not in enabled_import_methods | 404 | glance/api/v2/image_data.py:584-586 |
| "Unknown import method name '%s'." | import method not in enabled_import_methods | 400 | glance/api/v2/images.py:1709-1711 |
| "URI for web-download does not pass filtering: %s" / "URI for add location does not pass filtering: %s" | URL refused by [import_filtering_opts] | 400 | glance/api/v2/images.py:462-466, glance/api/v2/images.py:1195-1201 |
| "Error decoding your request. Either the URL or the request body contained characters that could not be decoded by Glance" | non-UTF-8 URL or body | 400 | glance/common/wsgi.py:511-515 |
Members, tags, tasks
| Exception / message | Situation | Client sees | Raised at |
|---|---|---|---|
| "Only shared images have members." | member operation on an image whose visibility is not shared | 403 | glance/api/v2/image_members.py:77-80 |
Duplicate "Member %(member_id)s is duplicated for image %(image_id)s" | the project is already a member | 409 | glance/api/v2/image_members.py:172-177 |
| "%(m_id)s not found in the member list of the image %(i_id)s." | unknown member | 404 | glance/api/v2/image_members.py:113-118 |
ValueError "Incorrect request: …" | member status other than pending, accepted, rejected | 400 | glance/api/v2/image_members.py:229-232, glance/domain/__init__.py:335-338 |
TaskNotFound | unknown or expired task | 404 | glance/db/sqlalchemy/api.py:1998 |
InvalidTaskType / InvalidTaskStatus | task type other than import, api_image_import, location_import | 400 [inferred: via Invalid] | glance/domain/__init__.py:364, glance/domain/__init__.py:367 |
| "This operation is currently not permitted on Glance Tasks. They are auto deleted after reaching the time based on their expires_at property." | DELETE /v2/tasks/{id} | 405 with Allow: GET | glance/api/v2/tasks.py:158-165 |
Metadata definitions
| Exception | Message (template) | Situation | Client sees | Raised at |
|---|---|---|---|---|
MetadefDuplicateNamespace (Duplicate) | "The metadata definition namespace=%(namespace_name)s already exists." | create or rename to an existing namespace | 409 | glance/db/sqlalchemy/metadef_api/namespace.py |
MetadefDuplicateObject, …Property, …Tag, …ResourceType, …ResourceTypeAssociation (Duplicate) | "… already exists in namespace=%(namespace_name)s." | name already used in the namespace | 409 | glance/db/sqlalchemy/metadef_api/ |
MetadefNamespaceNotFound, MetadefObjectNotFound, MetadefPropertyNotFound, MetadefTagNotFound, MetadefResourceTypeNotFound, MetadefResourceTypeAssociationNotFound (NotFound) | "… was not found …" | unknown name | 404 | glance/db/sqlalchemy/metadef_api/ |
MetadefForbidden (Forbidden) | "You are not authorized to complete this action." | private namespace of another project | 404 (hidden by the policy layer) or 403 | glance/db/sqlalchemy/metadef_api/namespace.py:99, glance/db/sqlalchemy/metadef_api/namespace.py:121 |
MetadefIntegrityError (Forbidden) | "The metadata definition %(record_type)s with name=%(record_name)s not deleted. Other records still refer to it." | delete of a namespace or resource type still referenced | 403 | glance/db/sqlalchemy/metadef_api/namespace.py:270, glance/db/sqlalchemy/metadef_api/resource_type.py:102 |
ProtectedMetadefResourceTypeSystemDelete (Forbidden) | "Metadata definition resource-type %(resource_type_name)s is a seeded-system type and cannot be deleted." | delete of a seeded resource type | 403 | glance/db/sqlalchemy/metadef_api/resource_type.py:92 |
ProtectedMetadefNamespaceDelete, …NamespacePropDelete, …ObjectDelete, …ResourceTypeAssociationDelete, …TagDelete (Forbidden) | "… is protected and cannot be deleted." | delete inside a protected namespace | 403 | glance/domain/__init__.py:544, glance/domain/__init__.py:582, glance/domain/__init__.py:616, glance/domain/__init__.py:645, glance/domain/__init__.py:672 |
Asynchronous errors
These never reach the client as an HTTP status. The client sees them through the image (status, os_glance_failed_import, os_glance_importing_to_stores) and the task (status = failure, message) returned by GET /v2/images/{id} and GET /v2/images/{id}/tasks.
| Situation | Visible effect | Log | Defined at |
|---|---|---|---|
| quota exceeded when the import starts | task failure, image back to queued | Failed to execute task [inferred] | glance/async_/flows/api_image_import.py:891 |
web-download URL unreachable or refused at fetch time | task failure, image queued | "Task %(task_id)s failed with exception %(error)s" | glance/async_/flows/_internal_plugins/web_download.py:51-67 |
glance-download: remote image not active | _InvalidGlanceDownloadImageStatus "Source image status should be active instead of %s" | same | glance/async_/flows/api_image_import.py:840-843 |
glance-download: size mismatch | ImportTaskError "Task %(task_id)s failed because downloaded data size %(data_size)i is different from expected %(expected)i" | — | glance/async_/flows/_internal_plugins/glance_download.py:72-78 |
glance-download: no endpoint for the region | GlanceEndpointNotFound "%(interface)s glance endpoint not found for region %(region)s" | — | glance/async_/utils.py:94 |
copy-image: no readable location | NotFound "Image not found in any configured store" | "Image: %(img_id)s is not present in store …" | glance/async_/flows/_internal_plugins/copy_image.py:108-114 |
conversion refused (format mismatch, VMDK type, qemu-img error) | RuntimeError with the reason; task failure | "Image claimed to be %s format but format inspection …" | glance/async_/flows/plugins/image_conversion.py:114-241 |
| all stores failed | _NoStoresSucceeded "None of the uploads finished!" | — | glance/async_/flows/api_image_import.py:735-739 |
one store failed with all_stores_must_succeed = false | store listed in os_glance_failed_import, image active | "%(task_id)s of %(task_type)s failed but since all_stores_must_succeed is set to false, continue." | glance/async_/flows/api_image_import.py:673-682 |
task no longer processing during a write | TaskAbortedError, write stopped | "Task %(task)s expected \"processing\" status, but found \"%(status)s\"; aborting." | glance/async_/flows/api_image_import.py:687-705 |
location hash failed http_retries times | _HashCalculationFailed; image back to queued if not active | — | glance/async_/flows/location_import.py:85-103 |
| hash calculation canceled by an image delete | _HashCalculationCanceled | — | glance/async_/flows/location_import.py:71-72 |
| validation data does not match the computed hash | InvalidParameterValue, image back to queued | — | glance/async_/flows/location_import.py:164-177 |
| import lock stolen (stale task) | old task failure "Expired lock preempted" | "Image %(image)s has stale import task %(task)s in status %(status)s from %(owner)s; removed lock because it had expired." | glance/api/v2/images.py:135-169 |
| scrubber cannot delete a location | location stays pending_delete, retried next run | "Failed to delete image %(image_id)s location …" [inferred: wording] | glance/scrubber.py:390-414 |
Key log messages
| Level | Message | Situation | Defined at |
|---|---|---|---|
| ERROR | "Caught error: %s" (with traceback) | any unexpected exception in a handler → 500 | glance/common/wsgi.py:520 |
| ERROR | "Failed to initialize oslo_limit, likely due to incorrect or insufficient configuration: %(err)s" | unified limits misconfigured | glance/quota/keystone.py:66 |
| WARNING | "Import methods are enabled but staging directory %(path)s does not exist; Imports will fail!" | startup without staging directory | glance/common/wsgi_app.py:159-162 |
| WARNING | "Image %(image)s has active import task %(task)s in status %(status)s; lock remains valid for %(expire)i more seconds" | import refused with 409 because of a valid lock | glance/api/v2/images.py:207-213 |
| WARNING | "Image claims %r disk_format but is actually %r" | raw upload whose content is a known format | glance/location.py:647 |
| ERROR | "Glance tried all active locations to get data for image %s but all have failed." | download or copy failed on every location | glance/location.py:733 |
| ERROR | "Failed to proxy to %r: %s" | the worker holding staged data cannot be reached | glance/api/v2/images.py:279, glance/api/v2/images.py:282 |
| INFO | "Unable to create trust: %s Use the existing user token." | upload without a trust (e.g. no Keystone token info) | glance/api/v2/image_data.py:156-158 |
| ERROR | "Checksum header is missing." | cache middleware cannot verify a response it would cache | glance/api/middleware/cache.py:275 |
Source: reverse-spec-glance/02-errors_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.