From ed4579469c6b094fc3c7d08432851123c80e2bfd Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Tue, 8 Aug 2023 12:31:09 -0600 Subject: [PATCH 1/3] fixes #827 --- docs/publish.md | 118 ++++++++++++++++++++++++++++++++++++++++++ docs/releases.md | 4 ++ docs/subscribe/api.md | 4 ++ server/errors.go | 1 + server/server.go | 16 ++++++ server/server_test.go | 3 +- server/types.go | 66 +++++++++++------------ 7 files changed, 179 insertions(+), 33 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index c03cc4a4..63ca82f9 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -1114,6 +1114,7 @@ all the supported fields: | `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery | | `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications | | `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) | +| `extras` | - | *JSON object* | `{"customField": "customValue"}` | Extra key:value pairs will be included in the notification | ## Action buttons _Supported on:_ :material-android: :material-apple: :material-firefox: @@ -2662,6 +2663,123 @@ Here's an example of how it will look on Android:
Custom icon from an external URL
+## Custom fields +_Supported on:_ :material-android: + +You can send custom key:value pairs that will be included as-is in the notification. This can be helpful if you are +using ntfy to pass messages between different computer programs or services, for example. Simply pass a stringified +JSON object in the `X-Extras` header, or include the JSON object in the `extras` key when using [JSON publishing] +(#publish-as-json). **The JSON object can only be 1 level deep, nesting is not supported**. + +Here's an example showing how to send custom fields: + +=== "Command line (curl) (JSON)" + ``` + curl ntfy.sh \ + -d '{ + "topic": "mytopic", + "message": "Disk space is low at 5.1 GB", + "title": "Low disk space alert", + "tags": ["warning","cd"], + "priority": 4, + "extras": {"lastChecked": "20230205"} + }' + ``` +=== "Command line (curl) (Header)" + ``` + curl \ + -H "Title: Low disk space alert" \ + -H "Tags: warning,cd" \ + -H "X-Priority: 4" \ + -H 'X-Extras: {"lastChecked": "20230205"}' \ + -d "Disk space is low at 5.1 GB" \ + ntfy.sh/mytopic + ``` + +=== "HTTP" + ``` http + POST /mytopic HTTP/1.1 + Host: ntfy.sh + Title: Low disk space alert + Tags: warning,cd + X-Priority: 4 + X-Extras: {"lastChecked": "20230205"} + + Disk space is low at 5.1 GB + ``` + +=== "JavaScript" + ``` javascript + fetch('https://ntfy.sh/mytopic', { + method: 'POST', + headers: { + 'Title': 'Low disk space alert', + 'Tags': 'warning,cd' + 'X-Priority': '4', + 'X-Extras': {'lastChecked': '20230205'} + }, + body: "Disk space is low at 5.1 GB" + }) + ``` + +=== "Go" + ``` go + req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", strings.NewReader("Disk space is low at 5.1 GB")) + req.Header.Set("Title", "Low disk space alert") + req.Header.Set("Tags", "warning,cd") + req.Header.Set("X-Priority", "4") + req.Header.Set("X-Extras", `{"lastChecked": "20230205"}`) + http.DefaultClient.Do(req) + ``` + +=== "PowerShell" + ``` powershell + $Request = @{ + Method = "POST" + URI = "https://ntfy.sh" + Body = @{ + Topic = "mytopic" + Title = "Low disk space alert" + Tags = @("warning", "cd") + Priority = 4 + Message = "Disk space is low at 5.1 GB" + Extras = ConvertTo-JSON @{ + lastChecked = "20230205" + } + } + ContentType = "application/json" + } + Invoke-RestMethod @Request + ``` + +=== "Python" + ``` python + requests.post("https://ntfy.sh/mytopic", + data="Disk space is low at 5.1 GB", + headers={ + "Title": "Low disk space alert", + "Tags": "warning,cd", + "X-Priority": "4", + "X-Extras": '{"lastChecked": "20230205"}' + }) + ``` + +=== "PHP" + ``` php-inline + file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([ + 'http' => [ + 'method' => 'PUT', + 'header' => + "Content-Type: text/plain\r\n" . // Does not matter + "Title: Low disk space alert\r\n" . + "Tags: warning,cd\r\n" . + "X-Priority: 4\r\n" . + "X-Extras: {\"lastChecked\": \"20230205\"}", + ], + 'content' => "Disk space is low at 5.1 GB" + ])); + ``` + ## E-mail notifications _Supported on:_ :material-android: :material-apple: :material-firefox: diff --git a/docs/releases.md b/docs/releases.md index 1e518c76..f8dfd64a 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1285,6 +1285,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ### ntfy server v2.8.0 (UNRELEASED) +**Features:** + +* You can now send custom fields within an `extras` field in a JSON POST/PUT request ([#827](https://github.com/binwiederhier/ntfy/issues/827), thanks to [@tka85](https://github.com/tka85) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) + **Bug fixes + maintenance:** * Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting) diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md index 58da9752..a7763f33 100644 --- a/docs/subscribe/api.md +++ b/docs/subscribe/api.md @@ -329,6 +329,7 @@ format of the message. It's very straight forward: | `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) | | `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification | | `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) | +| `extras` | - | *JSON object* | `{"customField": "customValue"}` | Extra key:value pairs provided by the publisher | **Attachment** (part of the message, see [attachments](../publish.md#attachments) for details): @@ -363,6 +364,9 @@ Here's an example for each message type: "expires": 1643946728, "url": "https://ntfy.sh/file/sPs71M8A2T.png" }, + "extras": { + "customField": "customValue" + }, "title": "Unauthorized access detected", "message": "Movement detected in the yard. You better go check" } diff --git a/server/errors.go b/server/errors.go index 27ba3df0..91ad026f 100644 --- a/server/errors.go +++ b/server/errors.go @@ -117,6 +117,7 @@ var ( errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil} errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil} errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil} + errHTTPBadRequestExtrasInvalid = &errHTTP{40041, http.StatusBadRequest, "invalid request: extras invalid", "", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/server/server.go b/server/server.go index 0ab36524..2256f45c 100644 --- a/server/server.go +++ b/server/server.go @@ -1010,6 +1010,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) } } + extrasStr := readParam(r, "x-extras") + if extrasStr != "" { + extras := make(map[string]string) + if err := json.Unmarshal([]byte(extrasStr), &extras); err != nil { + return false, false, "", "", false, errHTTPBadRequestExtrasInvalid.Wrap(e.Error()) + } + m.Extras = extras + } contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") if markdown || strings.ToLower(contentType) == "text/markdown" { m.ContentType = "text/markdown" @@ -1808,6 +1816,14 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { if m.Call != "" { r.Header.Set("X-Call", m.Call) } + if len(m.Extras) > 0 { + extrasStr, err := json.Marshal(m.Extras) + if err != nil { + return errHTTPBadRequestMessageJSONInvalid + } + r.Header.Set("X-Extras", string(extrasStr)) + } + return next(w, r, v) } } diff --git a/server/server_test.go b/server/server_test.go index 647268fb..42037980 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1555,7 +1555,7 @@ func TestServer_PublishAsJSON(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` + `"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` + - `"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min"}` + `"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min", "extras": {"customField":"foo"}}` response := request(t, s, "PUT", "/", body, nil) require.Equal(t, 200, response.Code) @@ -1569,6 +1569,7 @@ func TestServer_PublishAsJSON(t *testing.T) { require.Equal(t, "http://ntfy.sh", m.Click) require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon) require.Equal(t, "", m.ContentType) + require.Equal(t, map[string]string{"customField": "foo"}, m.Extras) require.Equal(t, 4, m.Priority) require.True(t, m.Time > time.Now().Unix()+29*60) diff --git a/server/types.go b/server/types.go index eeb566fc..ae99f94b 100644 --- a/server/types.go +++ b/server/types.go @@ -25,24 +25,25 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - Time int64 `json:"time"` // Unix time in seconds - Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) - Event string `json:"event"` // One of the above - Topic string `json:"topic"` - Title string `json:"title,omitempty"` - Message string `json:"message,omitempty"` - Priority int `json:"priority,omitempty"` - Tags []string `json:"tags,omitempty"` - Click string `json:"click,omitempty"` - Icon string `json:"icon,omitempty"` - Actions []*action `json:"actions,omitempty"` - Attachment *attachment `json:"attachment,omitempty"` - PollID string `json:"poll_id,omitempty"` - ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown - Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes - Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting - User string `json:"-"` // UserID of the uploader, used to associated attachments + ID string `json:"id"` // Random message ID + Time int64 `json:"time"` // Unix time in seconds + Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) + Event string `json:"event"` // One of the above + Topic string `json:"topic"` + Title string `json:"title,omitempty"` + Message string `json:"message,omitempty"` + Priority int `json:"priority,omitempty"` + Tags []string `json:"tags,omitempty"` + Click string `json:"click,omitempty"` + Icon string `json:"icon,omitempty"` + Actions []*action `json:"actions,omitempty"` + Attachment *attachment `json:"attachment,omitempty"` + PollID string `json:"poll_id,omitempty"` + ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown + Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes + Extras map[string]string `json:"extras,omitempty"` + Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting + User string `json:"-"` // UserID of the uploader, used to associated attachments } func (m *message) Context() log.Context { @@ -92,20 +93,21 @@ func newAction() *action { // publishMessage is used as input when publishing as JSON type publishMessage struct { - Topic string `json:"topic"` - Title string `json:"title"` - Message string `json:"message"` - Priority int `json:"priority"` - Tags []string `json:"tags"` - Click string `json:"click"` - Icon string `json:"icon"` - Actions []action `json:"actions"` - Attach string `json:"attach"` - Markdown bool `json:"markdown"` - Filename string `json:"filename"` - Email string `json:"email"` - Call string `json:"call"` - Delay string `json:"delay"` + Topic string `json:"topic"` + Title string `json:"title"` + Message string `json:"message"` + Priority int `json:"priority"` + Tags []string `json:"tags"` + Click string `json:"click"` + Icon string `json:"icon"` + Actions []action `json:"actions"` + Attach string `json:"attach"` + Markdown bool `json:"markdown"` + Filename string `json:"filename"` + Email string `json:"email"` + Call string `json:"call"` + Delay string `json:"delay"` + Extras map[string]string `json:"extras"` } // messageEncoder is a function that knows how to encode a message From 42c6d831df05b47f2b0015a6a39b63f049cae201 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Sun, 10 Sep 2023 10:50:12 -0600 Subject: [PATCH 2/3] store extras in cache --- server/message_cache.go | 60 ++++++++++++++++++++++++++++++------ server/message_cache_test.go | 12 +++++--- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index 8a613ff1..fb609ecd 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -46,6 +46,7 @@ const ( sender TEXT NOT NULL, user TEXT NOT NULL, content_type TEXT NOT NULL, + extras TEXT NOT NULL, encoding TEXT NOT NULL, published INT NOT NULL ); @@ -64,43 +65,43 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, extras, encoding, published) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesDueQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -122,7 +123,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 12 + currentSchemaVersion = 13 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -246,6 +247,11 @@ const ( migrate11To12AlterMessagesTableQuery = ` ALTER TABLE messages ADD COLUMN content_type TEXT NOT NULL DEFAULT(''); ` + + // 12 -> 13 + migrate12To13AlterMessagesTableQuery = ` + ALTER TABLE messages ADD COLUMN extras TEXT NOT NULL DEFAULT(''); + ` ) var ( @@ -262,6 +268,7 @@ var ( 9: migrateFrom9, 10: migrateFrom10, 11: migrateFrom11, + 12: migrateFrom12, } ) @@ -367,6 +374,14 @@ func (c *messageCache) addMessages(ms []*message) error { } actionsStr = string(actionsBytes) } + var extrasStr string + if len(m.Extras) > 0 { + extrasBytes, err := json.Marshal(m.Extras) + if err != nil { + return err + } + extrasStr = string(extrasBytes) + } var sender string if m.Sender.IsValid() { sender = m.Sender.String() @@ -392,6 +407,7 @@ func (c *messageCache) addMessages(ms []*message) error { sender, m.User, m.ContentType, + extrasStr, m.Encoding, published, ) @@ -664,7 +680,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 var priority int - var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string + var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, extrasStr, encoding string err := rows.Scan( &id, ×tamp, @@ -685,6 +701,7 @@ func readMessage(rows *sql.Rows) (*message, error) { &sender, &user, &contentType, + &extrasStr, &encoding, ) if err != nil { @@ -700,6 +717,12 @@ func readMessage(rows *sql.Rows) (*message, error) { return nil, err } } + var extras map[string]string + if extrasStr != "" { + if err := json.Unmarshal([]byte(extrasStr), &extras); err != nil { + return nil, err + } + } senderIP, err := netip.ParseAddr(sender) if err != nil { senderIP = netip.Addr{} // if no IP stored in database, return invalid address @@ -731,6 +754,7 @@ func readMessage(rows *sql.Rows) (*message, error) { Sender: senderIP, // Must parse assuming database must be correct User: user, ContentType: contentType, + Extras: extras, Encoding: encoding, }, nil } @@ -970,3 +994,19 @@ func migrateFrom11(db *sql.DB, _ time.Duration) error { } return tx.Commit() } + +func migrateFrom12(db *sql.DB, _ time.Duration) error { + log.Tag(tagMessageCache).Info("Migrating cache database schema: from 12 to 13") + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(migrate12To13AlterMessagesTableQuery); err != nil { + return err + } + if _, err := tx.Exec(updateSchemaVersion, 13); err != nil { + return err + } + return tx.Commit() +} diff --git a/server/message_cache_test.go b/server/message_cache_test.go index 79b7fc54..ff334ab0 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -143,25 +143,27 @@ func testCacheTopics(t *testing.T, c *messageCache) { require.Equal(t, "topic2", topics["topic2"].ID) } -func TestSqliteCache_MessagesTagsPrioAndTitle(t *testing.T) { - testCacheMessagesTagsPrioAndTitle(t, newSqliteTestCache(t)) +func TestSqliteCache_MessagesTagsPrioTitleAndExtras(t *testing.T) { + testCacheMessagesTagsPrioTitleAndExtras(t, newSqliteTestCache(t)) } -func TestMemCache_MessagesTagsPrioAndTitle(t *testing.T) { - testCacheMessagesTagsPrioAndTitle(t, newMemTestCache(t)) +func TestMemCache_MessagesTagsPrioTitleAndExtras(t *testing.T) { + testCacheMessagesTagsPrioTitleAndExtras(t, newMemTestCache(t)) } -func testCacheMessagesTagsPrioAndTitle(t *testing.T, c *messageCache) { +func testCacheMessagesTagsPrioTitleAndExtras(t *testing.T, c *messageCache) { m := newDefaultMessage("mytopic", "some message") m.Tags = []string{"tag1", "tag2"} m.Priority = 5 m.Title = "some title" + m.Extras = map[string]string{"foo": "bar"} require.Nil(t, c.AddMessage(m)) messages, _ := c.Messages("mytopic", sinceAllMessages, false) require.Equal(t, []string{"tag1", "tag2"}, messages[0].Tags) require.Equal(t, 5, messages[0].Priority) require.Equal(t, "some title", messages[0].Title) + require.Equal(t, map[string]string{"foo": "bar"}, messages[0].Extras) } func TestSqliteCache_MessagesSinceID(t *testing.T) { From 0ab5d20045af10104d4e12d22108449c882ca1e5 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Sun, 10 Sep 2023 11:00:29 -0600 Subject: [PATCH 3/3] include "extras" as a header option --- server/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index 2256f45c..582c693c 100644 --- a/server/server.go +++ b/server/server.go @@ -1010,7 +1010,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) } } - extrasStr := readParam(r, "x-extras") + extrasStr := readParam(r, "x-extras", "extras") if extrasStr != "" { extras := make(map[string]string) if err := json.Unmarshal([]byte(extrasStr), &extras); err != nil {