Skip to content

Commit 1238143

Browse files
authored
fix: Fixed a NoMethodError when a format declined to decode an http request (#63)
Signed-off-by: Daniel Azuma <[email protected]>
1 parent 44d02be commit 1238143

File tree

3 files changed

+137
-35
lines changed

3 files changed

+137
-35
lines changed

CHANGELOG.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
### v0.5.0 / 2021-06-28
44

5-
This is a significant update that reworks the interface of the JSON formatter and applies fixes to of its payload encoding behavior. There are several breaking changes to internal interfaces, as well as breaking fixes to the encoding/decoding behavior of HttpBinding class, and a few deprecations.
6-
7-
* FIXED: HttpBinding#decode_rack_env in binary content mode now parses JSON content-types and exposes the data attribute as a JSON value. Similarly, encoding in binary content mode serializes a JSON document even if the payload is string-typed. (BREAKING CHANGE)
8-
* CHANGED: HttpBinding#decode_rack_env raises CloudEvents::NotCloudEventError (rather than returning nil) if given an HTTP request that does not seem to have been intended as a CloudEvent. (BREAKING CHANGE)
5+
This is a significant update that provides several important spec-related and usability fixes. Some of the behavioral changes are breaking, so to preserve compatibility, new methods were added and old methods deprecated, particularly in the HttpBinding class. Additionally, the formatter interface has been simplified and expanded to support payload formatting.
6+
7+
* CHANGED: Deprecated HttpBinding#decode_rack_env and replaced with HttpBinding#decode_event. (The old method remains for backward compatibility, but will be removed in version 1.0). The new decode_event method has the following differences:
8+
* decode_event raises NotCloudEventError (rather than returning nil) if given an HTTP request that does not seem to have been intended as a CloudEvent.
9+
* decode_event takes an allow_opaque argument that, when set to true, returns a CloudEvents::Event::Opaque (rather than raising an exception) if given a structured event with a format not known by the SDK. Opaque event objects cannot have their fields inspected, but can be reserialized for retransmission to another event handler.
10+
* decode_event in binary content mode now parses JSON content-types and exposes the data attribute as a JSON value.
11+
* CHANGED: Deprecated the HttpBinding encoding entrypoints (encode_structured_content, encode_batched_content, and encode_binary_content) and replaced with a single encode_event entrypoint that handles all cases via the structured_format argument. (The old methods remain for backward compatibility, but will be removed in version 1.0). In addition, the new encode_event method has the following differences:
12+
* encode_event in binary content mode now interprets a string-valued data attribute as a JSON string and serializes it (i.e. wraps it in quotes) if the data_content_type has a JSON format. This is for compatibility with the behavior of the JSON structured mode which always treats the data attribute as a JSON value if the data_content_type indicates JSON.
913
* CHANGED: The JsonFormat class interface was reworked to be more generic, combine the structured and batched calls, and add calls to handle data payloads. A Format module has been added to specify the interface used by JsonFormat and future formatters. (Breaking change of internal interfaces)
10-
* ADDED: It is now possible to tell HttpBinding#decode_rack_env to return an opaque object if the request specifies a format that is not known by the SDK. This opaque object cannot have its fields inspected, but can be reserialized for retransmission to another event handler.
11-
* CHANGED: If opaque objects are disabled and an input request has an unknown format, HttpBinding#decode_rack_env now raises UnsupportedFormatError instead of HttpContentError. (The old exception name is aliased to the new name for backward compatibility, but is deprecated and will be removed in version 1.0.)
12-
* CHANGED: If format-driven parsing fails, HttpBinding#decode_rack_env will raise FormatSyntaxError instead of, for example, a JSON-specific error. The lower-level parser error can still be accessed as the "cause".
13-
* CHANGED: Consolidated HttpBinding encoding entrypoints into one HttpBinding#encode_event call. The older encode_structured_content, encode_batched_content, and encode_binary_content calls are now deprecated.
14+
* CHANGED: Renamed HttpContentError to UnsupportedFormatError to better reflect the specific issue being reported, and to eliminate the coupling to http. The old name remains aliased to the new name, but is deprecated and will be removed in version 1.0.
15+
* CHANGED: If format-driven parsing (e.g. parsing a JSON document) fails, a FormatSyntaxError will be raised instead of, for example, a JSON-specific error. The lower-level parser error can still be accessed from the exception's "cause".
1416
* FIXED: JsonFormat now sets the datacontenttype attribute explicitly to "application/json" if it isn't otherwise set.
1517

1618
### v0.4.0 / 2021-05-26

lib/cloud_events/http_binding.rb

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ class HttpBinding
2828
def self.default
2929
@default ||= begin
3030
http_binding = new
31-
json_format = JsonFormat.new
32-
http_binding.register_formatter json_format, JSON_FORMAT
31+
http_binding.register_formatter JsonFormat.new, JSON_FORMAT
3332
http_binding.default_encoder_name = JSON_FORMAT
3433
http_binding
3534
end
@@ -125,12 +124,12 @@ def register_formatter_methods formatter,
125124
# @raise [CloudEvents::CloudEventsError] if an event could not be decoded
126125
# from the request.
127126
#
128-
def decode_rack_env env, allow_opaque: false, **format_args
127+
def decode_event env, allow_opaque: false, **format_args
129128
content_type_string = env["CONTENT_TYPE"]
130129
content_type = ContentType.new content_type_string if content_type_string
131130
content = read_with_charset env["rack.input"], content_type&.charset
132-
result = decode_binary_content content, content_type, env
133-
result ||= decode_structured_content content, content_type, allow_opaque, **format_args
131+
result = decode_binary_content(content, content_type, env) ||
132+
decode_structured_content(content, content_type, allow_opaque, **format_args)
134133
if result.nil?
135134
content_type_string = content_type_string ? content_type_string.inspect : "not present"
136135
raise NotCloudEventError, "Content-Type is #{content_type_string}, and CE-SpecVersion is not present"
@@ -168,7 +167,7 @@ def encode_event event, structured_format: false, **format_args
168167
if event.is_a? ::Array
169168
raise ::ArgumentError, "Encoding a batch requires structured_format"
170169
end
171-
encode_binary_content event, **format_args
170+
encode_binary_content event, legacy_data_encode: false, **format_args
172171
else
173172
structured_format = default_encoder_name if structured_format == true
174173
raise ::ArgumentError, "Format name not specified, and no default is set" unless structured_format
@@ -183,6 +182,31 @@ def encode_event event, structured_format: false, **format_args
183182
end
184183
end
185184

185+
##
186+
# Decode an event from the given Rack environment hash. Following the
187+
# CloudEvents spec, this chooses a handler based on the Content-Type of
188+
# the request.
189+
#
190+
# @deprecated Will be removed in version 1.0. Use {#decode_event} instead.
191+
#
192+
# @param env [Hash] The Rack environment.
193+
# @param format_args [keywords] Extra args to pass to the formatter.
194+
# @return [CloudEvents::Event] if the request includes a single structured
195+
# or binary event.
196+
# @return [Array<CloudEvents::Event>] if the request includes a batch of
197+
# structured events.
198+
# @return [nil] if the request does not appear to be a CloudEvent.
199+
# @raise [CloudEvents::CloudEventsError] if the request appears to be a
200+
# CloudEvent but decoding failed.
201+
#
202+
def decode_rack_env env, **format_args
203+
content_type_string = env["CONTENT_TYPE"]
204+
content_type = ContentType.new content_type_string if content_type_string
205+
content = read_with_charset env["rack.input"], content_type&.charset
206+
decode_binary_content(content, content_type, env, legacy_data_decode: true) ||
207+
decode_structured_content(content, content_type, false, **format_args)
208+
end
209+
186210
##
187211
# Encode a single event in structured content mode in the given format.
188212
#
@@ -232,14 +256,28 @@ def encode_batched_content event_batch, format_name, **format_args
232256
# @param format_args [keywords] Extra args to pass to the formatter.
233257
# @return [Array(headers,String)]
234258
#
235-
def encode_binary_content event, **format_args
259+
def encode_binary_content event, legacy_data_encode: true, **format_args
236260
headers = {}
237261
event.to_h.each do |key, value|
238262
unless ["data", "datacontenttype"].include? key
239263
headers["CE-#{key}"] = percent_encode value
240264
end
241265
end
242-
body, content_type = encode_data event.spec_version, event.data, event.data_content_type, **format_args
266+
if legacy_data_encode
267+
body = event.data
268+
content_type = event.data_content_type&.to_s
269+
case body
270+
when ::String
271+
content_type ||= string_content_type body
272+
when nil
273+
content_type = nil
274+
else
275+
body = ::JSON.dump body
276+
content_type ||= "application/json; charset=#{body.encoding.name.downcase}"
277+
end
278+
else
279+
body, content_type = encode_data event.spec_version, event.data, event.data_content_type, **format_args
280+
end
243281
headers["Content-Type"] = content_type.to_s if content_type
244282
[headers, body]
245283
end
@@ -302,7 +340,7 @@ def add_named_formatter collection, formatter, name
302340
def decode_structured_content content, content_type, allow_opaque, **format_args
303341
@event_decoders.reverse_each do |decoder|
304342
result = decoder.decode_event content: content, content_type: content_type, **format_args
305-
event = result[:event] || result[:event_batch]
343+
event = result[:event] || result[:event_batch] if result
306344
return event if event
307345
end
308346
if content_type&.media_type == "application" &&
@@ -316,13 +354,20 @@ def decode_structured_content content, content_type, allow_opaque, **format_args
316354
##
317355
# Decode an event from the given Rack environment in binary content mode.
318356
#
319-
def decode_binary_content content, content_type, env
357+
# TODO: legacy_data_decode is deprecated and can be removed when
358+
# decode_rack_env is removed.
359+
#
360+
def decode_binary_content content, content_type, env, legacy_data_decode: false
320361
spec_version = env["HTTP_CE_SPECVERSION"]
321362
return nil unless spec_version
322363
unless spec_version =~ /^0\.3|1(\.|$)/
323364
raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
324365
end
325-
data, content_type = decode_data spec_version, content, content_type
366+
if legacy_data_decode
367+
data = content
368+
else
369+
data, content_type = decode_data spec_version, content, content_type
370+
end
326371
attributes = { "spec_version" => spec_version, "data" => data }
327372
attributes["data_content_type"] = content_type if content_type
328373
omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]

test/test_http_binding.rb

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
"rack.input" => StringIO.new(my_json_struct_encoded),
9393
"CONTENT_TYPE" => "application/cloudevents+json"
9494
}
95-
event = http_binding.decode_rack_env env
95+
event = http_binding.decode_event env
9696
assert_equal my_id, event.id
9797
assert_equal my_source, event.source
9898
assert_equal my_type, event.type
@@ -112,7 +112,7 @@
112112
"rack.input" => StringIO.new(my_json_batch_encoded),
113113
"CONTENT_TYPE" => "application/cloudevents-batch+json"
114114
}
115-
events = http_binding.decode_rack_env env
115+
events = http_binding.decode_event env
116116
assert_equal 1, events.size
117117
event = events.first
118118
assert_equal my_id, event.id
@@ -151,7 +151,7 @@
151151
"HTTP_CE_SUBJECT" => my_subject,
152152
"HTTP_CE_TIME" => my_time_string
153153
}
154-
event = http_binding.decode_rack_env env
154+
event = http_binding.decode_event env
155155
assert_equal my_id, event.id
156156
assert_equal my_source, event.source
157157
assert_equal my_type, event.type
@@ -171,7 +171,7 @@
171171
"rack.input" => StringIO.new(my_json_data_struct_encoded),
172172
"CONTENT_TYPE" => "application/cloudevents+json"
173173
}
174-
event = http_binding.decode_rack_env env
174+
event = http_binding.decode_event env
175175
assert_equal my_id, event.id
176176
assert_equal my_source, event.source
177177
assert_equal my_type, event.type
@@ -208,7 +208,7 @@
208208
"HTTP_CE_SUBJECT" => my_subject,
209209
"HTTP_CE_TIME" => my_time_string
210210
}
211-
event = http_binding.decode_rack_env env
211+
event = http_binding.decode_event env
212212
assert_equal my_id, event.id
213213
assert_equal my_source, event.source
214214
assert_equal my_type, event.type
@@ -223,6 +223,43 @@
223223
assert_equal my_json_data_struct_encoded, body
224224
end
225225

226+
it "decodes and re-encodes a binary JSON rack env using deprecated methods" do
227+
env = {
228+
"rack.input" => StringIO.new(my_simple_data),
229+
"HTTP_CE_ID" => my_id,
230+
"HTTP_CE_SOURCE" => my_source_string,
231+
"HTTP_CE_TYPE" => my_type,
232+
"HTTP_CE_SPECVERSION" => spec_version,
233+
"CONTENT_TYPE" => my_json_content_type_string,
234+
"HTTP_CE_DATASCHEMA" => my_schema_string,
235+
"HTTP_CE_SUBJECT" => my_subject,
236+
"HTTP_CE_TIME" => my_time_string
237+
}
238+
event = http_binding.decode_rack_env env
239+
assert_equal my_id, event.id
240+
assert_equal my_source, event.source
241+
assert_equal my_type, event.type
242+
assert_equal spec_version, event.spec_version
243+
assert_equal my_simple_data, event.data
244+
assert_equal my_json_content_type, event.data_content_type
245+
assert_equal my_schema, event.data_schema
246+
assert_equal my_subject, event.subject
247+
assert_equal my_time, event.time
248+
headers, body = http_binding.encode_binary_content event, sort: true
249+
expected_headers = {
250+
"CE-id" => my_id,
251+
"CE-source" => my_source_string,
252+
"CE-type" => my_type,
253+
"CE-specversion" => spec_version,
254+
"Content-Type" => my_json_content_type_string,
255+
"CE-dataschema" => my_schema_string,
256+
"CE-subject" => my_subject,
257+
"CE-time" => my_time_string
258+
}
259+
assert_equal expected_headers, headers
260+
assert_equal my_simple_data, body
261+
end
262+
226263
it "decodes a binary rack env using an InputWrapper and re-encodes as structured" do
227264
env = {
228265
"rack.input" => Rack::Lint::InputWrapper.new(StringIO.new(my_simple_data)),
@@ -235,7 +272,7 @@
235272
"HTTP_CE_SUBJECT" => my_subject,
236273
"HTTP_CE_TIME" => my_time_string
237274
}
238-
event = http_binding.decode_rack_env env
275+
event = http_binding.decode_event env
239276
assert_equal my_id, event.id
240277
assert_equal my_source, event.source
241278
assert_equal my_type, event.type
@@ -257,7 +294,7 @@
257294
"HTTP_CE_TYPE" => my_type,
258295
"HTTP_CE_SPECVERSION" => spec_version
259296
}
260-
event = http_binding.decode_rack_env env
297+
event = http_binding.decode_event env
261298
assert_equal my_id, event.id
262299
assert_equal my_source, event.source
263300
assert_equal my_type, event.type
@@ -289,7 +326,7 @@
289326
"HTTP_CE_SPECVERSION" => spec_version,
290327
"HTTP_CE_TRACECONTEXT" => my_trace_context
291328
}
292-
event = http_binding.decode_rack_env env
329+
event = http_binding.decode_event env
293330
assert_equal my_trace_context, event["tracecontext"]
294331
headers, body = http_binding.encode_event event
295332
expected_headers = {
@@ -330,17 +367,17 @@
330367
"HTTP_CE_TYPE" => encoded_weird_type,
331368
"HTTP_CE_SPECVERSION" => spec_version
332369
}
333-
reconstituted_event = http_binding.decode_rack_env env
370+
reconstituted_event = http_binding.decode_event env
334371
assert_equal event, reconstituted_event
335372
end
336373

337374
it "raises UnsupportedFormatError when a format is not recognized" do
338375
env = {
339376
"rack.input" => StringIO.new(my_json_struct_encoded),
340-
"CONTENT_TYPE" => "application/cloudevents+json"
377+
"CONTENT_TYPE" => "application/cloudevents+hello"
341378
}
342379
assert_raises CloudEvents::UnsupportedFormatError do
343-
minimal_http_binding.decode_rack_env env
380+
http_binding.decode_event env
344381
end
345382
end
346383

@@ -350,7 +387,7 @@
350387
"CONTENT_TYPE" => "application/cloudevents+json"
351388
}
352389
error = assert_raises CloudEvents::FormatSyntaxError do
353-
http_binding.decode_rack_env env
390+
http_binding.decode_event env
354391
end
355392
assert_kind_of JSON::ParserError, error.cause
356393
end
@@ -361,7 +398,7 @@
361398
"CONTENT_TYPE" => "application/cloudevents-batch+json"
362399
}
363400
error = assert_raises CloudEvents::FormatSyntaxError do
364-
http_binding.decode_rack_env env
401+
http_binding.decode_event env
365402
end
366403
assert_kind_of JSON::ParserError, error.cause
367404
end
@@ -374,16 +411,34 @@
374411
"HTTP_CE_SPECVERSION" => "0.1"
375412
}
376413
assert_raises CloudEvents::SpecVersionError do
377-
http_binding.decode_rack_env env
414+
http_binding.decode_event env
378415
end
379416
end
380417

418+
it "raises NotCloudEventError when a content-type is not recognized" do
419+
env = {
420+
"rack.input" => StringIO.new(my_json_struct_encoded),
421+
"CONTENT_TYPE" => "application/json"
422+
}
423+
assert_raises CloudEvents::NotCloudEventError do
424+
http_binding.decode_event env
425+
end
426+
end
427+
428+
it "returns nil from the legacy decode method when a content-type is not recognized" do
429+
env = {
430+
"rack.input" => StringIO.new(my_json_struct_encoded),
431+
"CONTENT_TYPE" => "application/json"
432+
}
433+
assert_nil http_binding.decode_rack_env env
434+
end
435+
381436
it "decodes and re-encodes a structured event using opaque" do
382437
env = {
383438
"rack.input" => StringIO.new(my_json_struct_encoded),
384439
"CONTENT_TYPE" => "application/cloudevents+json"
385440
}
386-
event = minimal_http_binding.decode_rack_env env, allow_opaque: true
441+
event = minimal_http_binding.decode_event env, allow_opaque: true
387442
assert_kind_of CloudEvents::Event::Opaque, event
388443
refute event.batch?
389444
headers, body = minimal_http_binding.encode_event event
@@ -396,7 +451,7 @@
396451
"rack.input" => StringIO.new(my_json_batch_encoded),
397452
"CONTENT_TYPE" => "application/cloudevents-batch+json"
398453
}
399-
event = minimal_http_binding.decode_rack_env env, allow_opaque: true
454+
event = minimal_http_binding.decode_event env, allow_opaque: true
400455
assert_kind_of CloudEvents::Event::Opaque, event
401456
assert event.batch?
402457
headers, body = minimal_http_binding.encode_event event

0 commit comments

Comments
 (0)