Skip to content

Commit b21e850

Browse files
lcianclaude
andauthored
fix: Include Content-Range header in 416 responses (#3)
Per RFC 7233 §4.4 and real GCS behavior, a 416 (Range Not Satisfiable) response should include Content-Range: bytes */<length> so clients know the actual object size and can adjust their requests. The testbench was raising the error before reaching the header-setting code, so the Content-Range header was never included. Fix by threading the object length through range_not_satisfiable() and RestException. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4e31e42 commit b21e850

3 files changed

Lines changed: 20 additions & 8 deletions

File tree

gcs/object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ def rest_media(self, request, delay=time.sleep):
504504
)
505505
# Return 416 if the requested range cannot be satisfied.
506506
if range_header is not None and begin >= length:
507-
testbench.error.range_not_satisfiable()
507+
testbench.error.range_not_satisfiable(length=length)
508508

509509
headers = {}
510510
content_range = "bytes %d-%d/%d" % (begin, end - 1, length)

testbench/error.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,24 @@
2323

2424

2525
class RestException(Exception):
26-
def __init__(self, msg, code):
26+
def __init__(self, msg, code, headers=None):
2727
super().__init__()
2828
self.msg = msg
2929
self.code = code
30+
self.headers = headers or {}
3031

3132
def as_response(self):
3233
# Include both code and message so we follow the schema outlined in
3334
# https://cloud.google.com/apis/design/errors#error_model and some
3435
# clients depend on code being specified, otherwise behavior is
3536
# undefined.
36-
return flask.make_response(
37+
response = flask.make_response(
3738
flask.jsonify(error={"code": self.code, "message": self.msg}),
3839
self.code,
3940
)
41+
for key, value in self.headers.items():
42+
response.headers[key] = value
43+
return response
4044

4145
@staticmethod
4246
def handler(ex):
@@ -53,12 +57,12 @@ def _simple_json_error(msg):
5357
return json.dumps({"error": {"errors": [{"domain": "global", "message": msg}]}})
5458

5559

56-
def generic(msg, rest_code, grpc_code, context):
60+
def generic(msg, rest_code, grpc_code, context, headers=None):
5761
"""Generate the appropriate error for REST or gRPC handlers."""
5862
if context is not None:
5963
context.abort(grpc_code, msg)
6064
else:
61-
raise RestException(msg, rest_code)
65+
raise RestException(msg, rest_code, headers=headers)
6266

6367

6468
def csek(context, rest_code=400, grpc_code=grpc.StatusCode.INVALID_ARGUMENT):
@@ -150,14 +154,20 @@ def already_exists(context=None):
150154

151155

152156
def range_not_satisfiable(
153-
context=None, rest_code=416, grpc_code=grpc.StatusCode.OUT_OF_RANGE
157+
length=None, context=None, rest_code=416, grpc_code=grpc.StatusCode.OUT_OF_RANGE
154158
):
155-
"""Error returned when request range is not satisfiable."""
159+
"""Error returned when request range is not satisfiable.
160+
161+
Includes a Content-Range: bytes */<length> header when length is provided,
162+
matching real GCS behavior per RFC 7233.
163+
"""
164+
headers = {"Content-Range": "bytes */%d" % length} if length is not None else None
156165
generic(
157166
_simple_json_error("request range not satisfiable"),
158167
rest_code,
159168
grpc_code,
160169
context,
170+
headers=headers,
161171
)
162172

163173

testbench/grpc_server.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,9 @@ def ReadObject(self, request, context):
593593
start = request.read_offset
594594
read_end = len(blob.media)
595595
if start > read_end:
596-
return testbench.error.range_not_satisfiable(context)
596+
return testbench.error.range_not_satisfiable(
597+
length=read_end, context=context
598+
)
597599
if request.read_limit > 0:
598600
read_end = min(read_end, start + request.read_limit)
599601
content_range = None

0 commit comments

Comments
 (0)