Skip to content

400 Bad Request when using Windows Push Notification Services (WNS) endpointΒ #162

@meewash-p

Description

@meewash-p

Hi,

I've encountered an issue when sending a push to the endpoint from WNS (MS Edge browser). Their service is responding with 400 Bad Request with no body.

After quick debugging, there was an error reason in response headers:

{
"Content-Length": "0",
"mise-correlation-id": "927f3b90-c2cf-4649-97a9-94330058c16d",
"X-WNS-ERROR-DESCRIPTION": "Ttl value conflicts with X-WNS-Cache-Policy.",
"X-WNS-STATUS": "dropped",
"X-WNS-NOTIFICATIONSTATUS": "dropped",
"Date": "Mon, 04 Mar 2024 12:24:23 GMT"
}

Turns out, WNS requires X-WNS-Cache-Policy request header set to either cache or no-cache depending on a ttl value.

Adding the header helps. Also, I'd suggest including headers in the exception message. :)

If you have that issue with WNS add x-wns-cache-policy header (if using ttl than x-wns-cache-policy: cache) in your code, e.g.:

webpush(
    subscription_info=push_subscription,
    data=payload_data,
    vapid_private_key=vapid_private_key,
    vapid_claims=vapid_claims,
    headers={'x-wns-cache-policy': 'no-cache'}
)

I'd do sth like this:

From a5442bd95eeb82835d3e7ecf62f11616d1cca8bd Mon Sep 17 00:00:00 2001
From: mp <[email protected]>
Date: Mon, 4 Mar 2024 13:40:55 +0100
Subject: [PATCH] Add headers.update with X-WNS-Cache-Policy and tests

---diff
 pywebpush/__init__.py           | 12 ++++++++++--
 pywebpush/tests/test_webpush.py | 21 +++++++++++++++++++++
 2 files changed, 31 insertions(+), 2 deletions(-)

--- a/pywebpush/__init__.py
+++ b/pywebpush/__init__.py
@@ -326,6 +326,14 @@ class WebPusher:
             headers.update({
                 'content-encoding': content_encoding,
             })
+            if ttl == 0:
+                headers.update({
+                    'x-wns-cache-policy': 'no-cache',
+                })
+            else:
+                headers.update({
+                    'x-wns-cache-policy': 'cache',
+                })
         if gcm_key:
             # guess if it is a legacy GCM project key or actual FCM key
             # gcm keys are all about 40 chars (use 100 for confidence),
@@ -494,7 +502,7 @@ def webpush(subscription_info,
         timeout=timeout,
     )
     if not curl and response.status_code > 202:
-        raise WebPushException("Push failed: {} {}\nResponse body:{}".format(
-            response.status_code, response.reason, response.text),
+        raise WebPushException("Push failed: {} {}\nResponse body:{}\nResponse headers: {}".format(
+            response.status_code, response.reason, response.text, str(response.headers)),
             response=response)
     return response
diff --git a/pywebpush/tests/test_webpush.py b/pywebpush/tests/test_webpush.py
index 93dc51a..938d9d1 100644
--- a/pywebpush/tests/test_webpush.py
+++ b/pywebpush/tests/test_webpush.py
@@ -142,6 +142,23 @@ class WebpushTestCase(unittest.TestCase):
         ckey = pheaders.get('crypto-key')
         assert 'pre-existing' in ckey
         assert pheaders.get('content-encoding') == 'aes128gcm'
+        assert pheaders.get('x-wns-cache-policy') == 'no-cache'
+
+    @patch("requests.post")
+    def test_send_ttl(self, mock_post):
+        subscription_info = self._gen_subscription_info()
+        headers = {"Crypto-Key": "pre-existing",
+                   "Authentication": "bearer vapid"}
+        data = "Mary had a little lamb"
+        WebPusher(subscription_info).send(data, headers, ttl=10)
+        assert subscription_info.get('endpoint') == mock_post.call_args[0][0]
+        pheaders = mock_post.call_args[1].get('headers')
+        assert pheaders.get('ttl') == '10'
+        assert pheaders.get('AUTHENTICATION') == headers.get('Authentication')
+        ckey = pheaders.get('crypto-key')
+        assert 'pre-existing' in ckey
+        assert pheaders.get('content-encoding') == 'aes128gcm'
+        assert pheaders.get('x-wns-cache-policy') == 'cache'
 
     @patch("requests.post")
     def test_send_vapid(self, mock_post):
@@ -173,6 +190,7 @@ class WebpushTestCase(unittest.TestCase):
         ckey = pheaders.get('crypto-key')
         assert 'dh=' in ckey
         assert pheaders.get('content-encoding') == 'aesgcm'
+        assert pheaders.get('x-wns-cache-policy') == 'no-cache'
         assert pheaders.get('test-header') == 'test-value'
 
     @patch.object(WebPusher, "send")
@@ -288,6 +306,7 @@ class WebpushTestCase(unittest.TestCase):
         pheaders = mock_post.call_args[1].get('headers')
         assert pheaders.get('ttl') == '0'
         assert pheaders.get('content-encoding') == 'aes128gcm'
+        assert pheaders.get('x-wns-cache-policy') == 'no-cache'
 
     @patch("pywebpush.open")
     def test_as_curl(self, opener):
@@ -334,6 +353,7 @@ class WebpushTestCase(unittest.TestCase):
         assert pdata["registration_ids"][0] == "regid123"
         assert pheaders.get("authorization") == "key=gcm_key_value"
         assert pheaders.get("content-type") == "application/json"
+        assert pheaders.get('x-wns-cache-policy') == 'no-cache'
 
     @patch("requests.post")
     def test_timeout(self, mock_post):
@@ -360,6 +380,7 @@ class WebpushTestCase(unittest.TestCase):
         ckey = pheaders.get('crypto-key')
         assert 'pre-existing' in ckey
         assert pheaders.get('content-encoding') == 'aes128gcm'
+        assert pheaders.get('x-wns-cache-policy') == 'no-cache'
 
 
 class WebpushExceptionTestCase(unittest.TestCase):
-- 
2.34.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions