Skip to content

Commit 77737c0

Browse files
rockwotjhiranya911
authored andcommitted
Add header for opting into correct URL decoding (#206)
* Add header for opting into correct URL decoding There is a long standing bug (~years) in the RTDB url decoding where we URL decode query parameters twice. Since we can't simply fix the issue without breaking users, we allow an opt-in upgrade/fix of the correct behavior via a header `X-Firebase-Decoding: 1`. We hope to make this the default (correct) behavior at some point in the near future. Which then this header will not be needed anymore. * Add tests for X-Firebase-Decoding
1 parent cd38b6f commit 77737c0

File tree

2 files changed

+22
-2
lines changed

2 files changed

+22
-2
lines changed

firebase_admin/db.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737

3838
_DB_ATTRIBUTE = '_database'
39-
_INVALID_PATH_CHARACTERS = '[].?#$'
39+
_INVALID_PATH_CHARACTERS = '[].#$'
4040
_RESERVED_FILTERS = ('$key', '$value', '$priority')
4141
_USER_AGENT = 'Firebase/HTTP/{0}/{1}.{2}/AdminPython'.format(
4242
firebase_admin.__version__, sys.version_info.major, sys.version_info.minor)
@@ -845,7 +845,8 @@ def __init__(self, credential, base_url, auth_override, timeout):
845845
timeout, which is the default behavior of the underlying requests library.
846846
"""
847847
_http_client.JsonHttpClient.__init__(
848-
self, credential=credential, base_url=base_url, headers={'User-Agent': _USER_AGENT})
848+
self, credential=credential, base_url=base_url,
849+
headers={'User-Agent': _USER_AGENT, 'X-Firebase-Decoding': '1'})
849850
self.credential = credential
850851
self.auth_override = auth_override
851852
self.timeout = timeout

tests/test_db.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def test_get_value(self, data):
157157
assert recorder[0].url == 'https://test.firebaseio.com/test.json'
158158
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
159159
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
160+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
160161
assert 'X-Firebase-ETag' not in recorder[0].headers
161162

162163
@pytest.mark.parametrize('data', valid_values)
@@ -169,6 +170,7 @@ def test_get_with_etag(self, data):
169170
assert recorder[0].url == 'https://test.firebaseio.com/test.json'
170171
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
171172
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
173+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
172174
assert recorder[0].headers['X-Firebase-ETag'] == 'true'
173175

174176
@pytest.mark.parametrize('data', valid_values)
@@ -180,6 +182,7 @@ def test_get_shallow(self, data):
180182
assert recorder[0].method == 'GET'
181183
assert recorder[0].url == 'https://test.firebaseio.com/test.json?shallow=true'
182184
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
185+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
183186
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
184187

185188
def test_get_with_etag_and_shallow(self):
@@ -197,12 +200,14 @@ def test_get_if_changed(self, data):
197200
assert recorder[0].method == 'GET'
198201
assert recorder[0].url == 'https://test.firebaseio.com/test.json'
199202
assert recorder[0].headers['if-none-match'] == 'invalid-etag'
203+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
200204

201205
assert ref.get_if_changed(MockAdapter.ETAG) == (False, None, None)
202206
assert len(recorder) == 2
203207
assert recorder[1].method == 'GET'
204208
assert recorder[1].url == 'https://test.firebaseio.com/test.json'
205209
assert recorder[1].headers['if-none-match'] == MockAdapter.ETAG
210+
assert recorder[1].headers['X-Firebase-Decoding'] == '1'
206211

207212
@pytest.mark.parametrize('etag', [0, 1, True, False, dict(), list(), tuple()])
208213
def test_get_if_changed_invalid_etag(self, etag):
@@ -221,6 +226,7 @@ def test_order_by_query(self, data):
221226
assert recorder[0].method == 'GET'
222227
assert recorder[0].url == 'https://test.firebaseio.com/test.json?' + query_str
223228
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
229+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
224230

225231
@pytest.mark.parametrize('data', valid_values)
226232
def test_limit_query(self, data):
@@ -234,6 +240,7 @@ def test_limit_query(self, data):
234240
assert recorder[0].method == 'GET'
235241
assert recorder[0].url == 'https://test.firebaseio.com/test.json?' + query_str
236242
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
243+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
237244

238245
@pytest.mark.parametrize('data', valid_values)
239246
def test_range_query(self, data):
@@ -248,6 +255,7 @@ def test_range_query(self, data):
248255
assert recorder[0].method == 'GET'
249256
assert recorder[0].url == 'https://test.firebaseio.com/test.json?' + query_str
250257
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
258+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
251259

252260
@pytest.mark.parametrize('data', valid_values)
253261
def test_set_value(self, data):
@@ -259,6 +267,7 @@ def test_set_value(self, data):
259267
assert recorder[0].url == 'https://test.firebaseio.com/test.json?print=silent'
260268
assert json.loads(recorder[0].body.decode()) == data
261269
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
270+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
262271

263272
def test_set_none_value(self):
264273
ref = db.reference('/test')
@@ -285,6 +294,7 @@ def test_update_children(self, data):
285294
assert recorder[0].url == 'https://test.firebaseio.com/test.json?print=silent'
286295
assert json.loads(recorder[0].body.decode()) == data
287296
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
297+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
288298

289299
@pytest.mark.parametrize('data', valid_values)
290300
def test_set_if_unchanged_success(self, data):
@@ -298,6 +308,7 @@ def test_set_if_unchanged_success(self, data):
298308
assert json.loads(recorder[0].body.decode()) == data
299309
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
300310
assert recorder[0].headers['if-match'] == MockAdapter.ETAG
311+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
301312

302313
@pytest.mark.parametrize('data', valid_values)
303314
def test_set_if_unchanged_failure(self, data):
@@ -311,6 +322,7 @@ def test_set_if_unchanged_failure(self, data):
311322
assert json.loads(recorder[0].body.decode()) == data
312323
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
313324
assert recorder[0].headers['if-match'] == 'invalid-etag'
325+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
314326

315327
@pytest.mark.parametrize('etag', [0, 1, True, False, dict(), list(), tuple()])
316328
def test_set_if_unchanged_invalid_etag(self, etag):
@@ -356,6 +368,7 @@ def test_push(self, data):
356368
assert json.loads(recorder[0].body.decode()) == data
357369
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
358370
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
371+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
359372

360373
def test_push_default(self):
361374
ref = db.reference('/test')
@@ -367,6 +380,7 @@ def test_push_default(self):
367380
assert json.loads(recorder[0].body.decode()) == ''
368381
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
369382
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
383+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
370384

371385
def test_push_none_value(self):
372386
ref = db.reference('/test')
@@ -383,6 +397,7 @@ def test_delete(self):
383397
assert recorder[0].url == 'https://test.firebaseio.com/test.json'
384398
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
385399
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
400+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
386401

387402
def test_transaction(self):
388403
ref = db.reference('/test')
@@ -568,6 +583,7 @@ def test_get_value(self):
568583
assert recorder[0].url == 'https://test.firebaseio.com/test.json?' + query_str
569584
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
570585
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
586+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
571587

572588
def test_set_value(self):
573589
ref = db.reference('/test')
@@ -581,6 +597,7 @@ def test_set_value(self):
581597
assert json.loads(recorder[0].body.decode()) == data
582598
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
583599
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
600+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
584601

585602
def test_order_by_query(self):
586603
ref = db.reference('/test')
@@ -593,6 +610,7 @@ def test_order_by_query(self):
593610
assert recorder[0].url == 'https://test.firebaseio.com/test.json?' + query_str
594611
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
595612
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
613+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
596614

597615
def test_range_query(self):
598616
ref = db.reference('/test')
@@ -606,6 +624,7 @@ def test_range_query(self):
606624
assert recorder[0].url == 'https://test.firebaseio.com/test.json?' + query_str
607625
assert recorder[0].headers['Authorization'] == 'Bearer mock-token'
608626
assert recorder[0].headers['User-Agent'] == db._USER_AGENT
627+
assert recorder[0].headers['X-Firebase-Decoding'] == '1'
609628

610629

611630
class TestDatabaseInitialization(object):

0 commit comments

Comments
 (0)