Module: HotCell::Payload

Defined in:
lib/hot_cell/payload.rb

Overview

A payload is a JSON object, and the rules are stricter than JSON's in one direction and looser in another.

Values must be JSON-native, because to_json is not a check. It serializes a Symbol to a String, a Time to a String, and an arbitrary object through whatever to_json that object happens to define, all silently and none of it faithfully. So validate the values, then serialize.

Keys may be Strings or Symbols, because both serialize to the same JSON string and both arrive symbolized. { format: "png" } is how a payload is naturally written and must not be rejected.

Constant Summary collapse

MAX_DEPTH =

A payload sits one level inside the message envelope, so it gets one level less than the line.

MAX_NESTING - 1

Class Method Summary collapse

Class Method Details

.generate(object, name) ⇒ Object



20
21
22
23
# File 'lib/hot_cell/payload.rb', line 20

def generate(object, name)
  validate! object, name
  JSON.generate object
end

.parse(json) ⇒ Object

Keys are deep-symbolized here rather than in an operation, so an operation never has to know whether to reach for payload or payload, and a nested hash can be splatted straight into a library's keyword arguments. Only keys: a Symbol value would not survive the round trip, which the JSON-native rule already forbids.

JSON.parse only. Never JSON.load, and never create_additions, both of which instantiate arbitrary classes named by a json_class key in the document.

Every way this can fail becomes one named failure, and the catch-all is the point rather than laziness. Callers used to name what JSON.parse raises, and naming it has now been wrong twice: a report that was not an object raised TypeError past a rescue for JSON::ParserError, and a key holding bytes that are not valid UTF-8 raises EncodingError past both. The supervisor reads worker reports through here inside the loop that enforces every request's deadline, and nothing above it rescues anything, so each miss is a one-line denial of service against every request in the cell.

The body is a single JSON.parse call, so this is scoped to "the JSON layer failed" and cannot swallow a bug in our own code. NoMemoryError is deliberately not caught: it is not a StandardError, and a document large enough to raise it is the worker's own memory verdict rather than a bad line.



43
44
45
46
47
48
# File 'lib/hot_cell/payload.rb', line 43

def parse(json)
  JSON.parse json, symbolize_names: true, max_nesting: MAX_NESTING, allow_nan: false,
                   create_additions: false
rescue StandardError => error
  raise MessageError, "#{error.class}: #{Failure.sanitize(error.message)}"
end

.validate!(object, name) ⇒ Object



50
51
52
53
54
55
56
57
# File 'lib/hot_cell/payload.rb', line 50

def validate!(object, name)
  unless object.is_a?(Hash)
    raise SerializationError, "#{name} is a #{object.class} and must be a Hash"
  end

  walk object, name, 1
  object
end