docusealco/docuseal · error · Templates::ModifyDocuments::InvalidLayout

Templates::ModifyDocuments::InvalidLayout

Error message

Templates::ModifyDocuments::InvalidLayout

What it means

Raised by Templates::ModifyDocuments.validate_layout! when the documents_layout argument passed to Templates::ModifyDocuments.call(template, documents_layout) is blank (nil or an empty array). The layout is the authoritative description of the template's new document structure, so 'nothing' is treated as a malformed request rather than 'no change'.

Source

Thrown at lib/templates/modify_documents.rb:93

            schema_item.except('google_drive_file_id').merge('attachment_uuid' => document.uuid)
          end
        end
      ensure
        sources.each_value(&:close)
      end
    end

    def add_page_mapping(mapping, ref, target)
      mapping[[ref['attachment_uuid'], ref['page']]] = target

      replaced = ref['replaced_page']

      mapping[[replaced['attachment_uuid'], replaced['page']]] = target if replaced
    end

    def validate_layout!(template, documents_layout, attachments_index)
      raise InvalidLayout if documents_layout.blank?
      raise InvalidLayout if documents_layout.all? { |entry| entry['pages'].blank? }

      dynamic_uuids = template.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid')
      non_dynamic_uuids = template.schema.pluck('attachment_uuid') - dynamic_uuids
      layout_uuids = documents_layout.pluck('attachment_uuid')

      raise InvalidLayout if layout_uuids.uniq.size != layout_uuids.size
      raise InvalidLayout if (non_dynamic_uuids - layout_uuids).any?
      raise InvalidLayout if layout_uuids.intersect?(dynamic_uuids)
      raise InvalidLayout if layout_uuids.any? { |uuid| attachments_index[uuid].nil? }

      refs = documents_layout.flat_map { |entry| entry['pages'].to_a }

      refs.each { |ref| validate_ref!(ref, attachments_index) }

      ref_keys = refs.map { |ref| [ref['attachment_uuid'], ref['page']] }

      raise InvalidLayout if ref_keys.uniq.size != ref_keys.size

View on GitHub (pinned to 004a22c1c8)

Solutions

  1. Skip the ModifyDocuments call entirely when the layout is blank — blank means there is nothing to apply
  2. Guard at the call site: return template unless documents_layout.is_a?(Array) && documents_layout.present?
  3. Fix the client to always send the full layout array whenever it requests a document modification

Example fix

# before
Templates::ModifyDocuments.call(template, params[:documents_layout].presence) # nil -> InvalidLayout
# after
layout = params[:documents_layout]
Templates::ModifyDocuments.call(template, layout) if layout.is_a?(Array) && layout.present?
Defensive patterns

Strategy: validation

Validate before calling

layout.is_a?(Array) && layout.present?

Type guard

layout.is_a?(Array) && !layout.empty?

Try / catch

begin
  Templates::ModifyDocuments.call(template, documents_layout)
rescue Templates::ModifyDocuments::InvalidLayout => e
  render json: { error: 'Invalid document layout' }, status: :unprocessable_entity
end

Prevention

When it happens

Trigger: Calling Templates::ModifyDocuments.call(template, nil) or (template, []) — e.g. an API client omits documents_layout from the request body, or upstream code passes params[:documents_layout].presence, which collapses an empty array to nil.

Common situations: A document-editor 'apply changes' request sent with an empty body; serialization that drops empty arrays; a guard elsewhere using .presence turning [] into nil; automated jobs running the modifier for every template including ones with no pending edits.

Related errors


AI-assisted analysis of docusealco/docuseal@004a22c1c8 (2026-08-21). Data as JSON: /api/errors/684ce16449656252. Report an issue: GitHub.