GeoServer 2.x REST API quirks
This is reference material. Each quirk lists the symptom, the root cause as understood, and the file:line in this repo where the workaround lives. Whenever you change any code that talks to GeoServer, scan this list for relevance.
1. Workspace-scoped POST /workspaces/{ws}/styles requires Accept: */*
- Symptom: GeoServer 2.28 returns
500 "No such style handler: format = application/json"if you sendAccept: application/json. - Root cause: GeoServer dispatches on the Accept header looking for a "style format handler" that matches that media type. There is no JSON style handler, so it 500s.
- Workaround: Send
Accept: "*/*"to disable the dispatch and route to the metadata-creation path. The body'sContent-Typeshould also beapplication/json; charset=utf-8— bareapplication/json500s in some older 2.x versions. - Where:
rest/styles/styles.go:141-173(Create— workspace-scoped branch setsaccept = "*/*").
2. Empty styles collection comes back as {"styles":""} (bare string)
- Symptom:
GetStylesagainst a fresh / empty workspace fails to unmarshal because GeoServer returns a JSON object whosestylesfield is the empty string instead of an object. - Workaround: Decode into
json.RawMessagefirst; branch on the first byte ('"'⇒ empty list;'{'⇒ decode as{"style": [...]}). - Where:
rest/styles/styles.go:85(json.RawMessagedecode tolerates the empty-string shape).
3. LayerGroup.styles.style is a mixed [string|object] array
- Symptom:
GET /layergroups/{name}for any layer group with default-styled members produces"styles": {"style": ["", "", {...}, ""]}— string entries (often empty) interspersed with style objects. Standard JSON decoder errors withcannot unmarshal string into Go struct field LayerGroupStyles.style. - Workaround: Custom
UnmarshalJSONonLayerGroupStylesthat decodes the innerstylearray via[]json.RawMessage, then per-element handles'"'(string) vs'{'(object) and produces[]*Resource. String entries are stored as&Resource{Name: stringValue}to preserve the[]*Resourcefield type. - Where:
rest/layergroups/types.go:108(Styles.UnmarshalJSON); the same trick handles the mixedPublishedshape atrest/layergroups/types.go:60.
4. POST style endpoints need explicit ; charset=utf-8
- Symptom: Bare
Content-Type: application/json500s in some 2.x versions. - Workaround: Send
Content-Type: application/json; charset=utf-8. - Where: Same site as quirk #1 —
rest/styles/styles.go:173(the body Content-Type is set to"application/json; charset=utf-8").
5. PostGIS publish requires the table to exist with attributes
- Symptom:
POST /workspaces/{ws}/datastores/{ds}/featuretypesreturns400 "no attributes"if the named table is empty or doesn't exist. - Workaround: Tests bootstrap a real PostGIS table via
docker/postgis/init/01-lbldyt.sql(createspublic.lbldyt(gid, name, label, geom)with sample rows + GIST index). Production callers must ensure their target table exists. - Where:
docker/postgis/init/01-lbldyt.sql; tested inrest/featuretypes/featuretypes_integration_test.go.
6. Settings.contact returns the empty string when absent
- Symptom:
GET /rest/settingson a freshly initialized GeoServer returns"contact": ""(bare string) instead of an empty object. Standard JSON decoding into a*Contactfield fails withcannot unmarshal string into Go struct field. - Workaround:
*Contactships a customUnmarshalJSONthat treats the empty-string and absent-field cases as a zero-valueContact, and decodes into the struct otherwise. - Where:
rest/settings/types.go(Contact.UnmarshalJSON); regression-guarded byrest/settings/settings_test.go:32(TestContact_UnmarshalEmptyString).
7. Pagination drift across versions
- Symptom:
GET /rest/layersandGET /rest/stylespaginate via?startIndex=&count=on GeoServer 2.18+ but return everything on older versions. - Workaround: Send pagination params and tolerate them being ignored on older servers. The client wraps this in
iter.Seq2[T, error]with single-page fallback so callers iterate uniformly across versions. - Where:
rest/styles/styles.go:104(Client.Iter),rest/layers/layers.go:83(WorkspaceClient.Iter).
8. URL building must escape per segment, not the whole path
- Symptom: Workspace / layer names with spaces, slashes, or non-ASCII characters produce malformed URLs if a caller
fmt.Sprintfs the path together. Literal*wildcards in ACL rule strings are rejected by GeoServer'sStrictHttpFirewallif double-encoded. - Workaround:
transport.BuildURL(base, parts)appliesurl.PathEscapeto each segment before joining and preserves the encoding through(*url.URL).String()by settingRawPathalongsidePath. Sub-clients reach it throughcoreAdapter.URL(parts...)(geoserver.go:422); neverfmt.SprintfREST paths. - Where:
internal/transport/url.go(BuildURL); regression-guarded byinternal/transport/url_test.go.
9. Empty wfs:FeatureType lists in capabilities
- Symptom: Older GeoServer versions emit
<FeatureTypeList></FeatureTypeList>(empty) while newer ones omit the element entirely. - Workaround: The WFS capabilities XML decoder treats both as an empty
FeatureTypeList; no caller-visible difference. The WMS side does the same for emptyLayerlists. - Where:
ows/wfs/types.go:106(FeatureTypeList);ows/wms/wms.go:114(ParseCapabilities) returns(*Capabilities, error).
When this skill is most useful
- Implementing a new REST resource client — scan the list before writing the request to avoid re-discovering quirk 1, 4, or 8.
- Debugging an integration-test failure with
unmarshalor5xxin the message. - Reading a PR that touches
rest/styles/,rest/layergroups/, orinternal/transport/url.go— verify the quirk-handling code wasn't naively "simplified" away.
Source: hishamkaram/geoserver — distributed by TomeVault.