util/error.lua
author Kim Alvefur <zash@zash.se>
Sat, 23 Mar 2024 20:48:19 +0100
changeset 13465 c673ff1075bd
parent 13083 e7a5e5a0dc02
permissions -rw-r--r--
mod_posix: Move everything to util.startup This allows greater control over the order of events. Notably, the internal ordering between daemonization, initialization of libunbound and setup of signal handling is sensitive. libunbound starts a separate thread for processing DNS requests. If this thread is started before signal handling has been set up, it will not inherit the signal handlers and instead behave as it would have before signal handlers were set up, i.e. cause the whole process to immediately exit. libunbound is usually initialized on the first DNS request, usually triggered by an outgoing s2s connection attempt. If daemonization happens before signals have been set up, signals may not be processed at all.

local id = require "prosody.util.id";

local util_debug; -- only imported on-demand

-- Library configuration (see configure())
local auto_inject_traceback = false;

local error_mt = { __name = "error" };

function error_mt:__tostring()
	return ("error<%s:%s:%s>"):format(self.type, self.condition, self.text or "");
end

local function is_error(e)
	return getmetatable(e) == error_mt;
end

local function configure(opt)
	if opt.auto_inject_traceback ~= nil then
		auto_inject_traceback = opt.auto_inject_traceback;
		if auto_inject_traceback then
			util_debug = require "prosody.util.debug";
		end
	end
end

-- Do we want any more well-known fields?
-- Or could we just copy all fields from `e`?
-- Sometimes you want variable details in the `text`, how to handle that?
-- Translations?
-- Should the `type` be restricted to the stanza error types or free-form?
-- What to set `type` to for stream errors or SASL errors? Those don't have a 'type' attr.

local function new(e, context, registry, source)
	if is_error(e) then return e; end
	local template = registry and registry[e];
	if not template then
		if type(e) == "table" then
			template = {
				code = e.code;
				type = e.type;
				condition = e.condition;
				text = e.text;
				extra = e.extra;
			};
		else
			template = {};
		end
	end
	context = context or {};

	if auto_inject_traceback then
		context.traceback = util_debug.get_traceback_table(nil, 2);
	end

	local error_instance = setmetatable({
		instance_id = id.short();

		type = template.type or "cancel";
		condition = template.condition or "undefined-condition";
		text = template.text;
		code = template.code;
		extra = template.extra;

		context = context;
		source = source;
	}, error_mt);

	return error_instance;
end

-- compact --> normal form
local function expand_registry(namespace, registry)
	local mapped = {}
	for err,template in pairs(registry) do
		local e = {
			type = template[1];
			condition = template[2];
			text = template[3];
		};
		if namespace and template[4] then
			e.extra = { namespace = namespace, condition = template[4] };
		end
		mapped[err] = e;
	end
	return mapped;
end

local function init(source, namespace, registry)
	if type(namespace) == "table" then
		-- registry can be given as second argument if namespace is not used
		registry, namespace = namespace, nil;
	end
	local _, protoerr = next(registry, nil);
	if protoerr and type(next(protoerr)) == "number" then
		registry = expand_registry(namespace, registry);
	end

	local function wrap(e, context)
		if is_error(e) then
			return e;
		end
		local err = new(registry[e] or {
			type = "cancel", condition = "undefined-condition"
		}, context, registry, source);
		err.context.wrapped_error = e;
		return err;
	end

	return {
		source = source;
		registry = registry;
		new = function (e, context)
			return new(e, context, registry, source);
		end;
		coerce = function (ok, err, ...)
			if ok then
				return ok, err, ...;
			end
			return nil, wrap(err);
		end;
		wrap = wrap;
		is_error = is_error;
	};
end

local function coerce(ok, err, ...)
	if ok or is_error(err) then
		return ok, err, ...;
	end

	local new_err = new({
		type = "cancel", condition = "undefined-condition"
	}, { wrapped_error = err });

	return ok, new_err, ...;
end

local function from_stanza(stanza, context, source)
	local error_type, condition, text, extra_tag = stanza:get_error();
	local error_tag = stanza:get_child("error");
	context = context or {};
	context.stanza = stanza;
	context.by = error_tag and error_tag.attr.by or stanza.attr.from;

	local uri;
	if condition == "gone" or condition == "redirect" then
		uri = error_tag:get_child_text(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
	end

	return new({
		type = error_type or "cancel";
		condition = condition or "undefined-condition";
		text = text;
		extra = (extra_tag or uri) and {
			uri = uri;
			tag = extra_tag;
		} or nil;
	}, context, nil, source);
end

return {
	new = new;
	init = init;
	coerce = coerce;
	is_error = is_error;
	is_err = is_error; -- COMPAT w/ older 0.12 trunk
	from_stanza = from_stanza;
	configure = configure;
}