Table of Contents

Class QuartzHttpApiOptions

Namespace
Quartz
Assembly
Quartz.AspNetCore.dll

How the HTTP API is served.

public sealed class QuartzHttpApiOptions
Inheritance
QuartzHttpApiOptions
Inherited Members

Remarks

There is one set of these per process, not one per scheduler. The API serves every scheduler in the container through one set of endpoints — a request names the scheduler it is for — so what is configured here describes the endpoints rather than any scheduler. That is why there is no IQuartzBuilder registration: calling services.AddQuartzHttpApi(configure) twice configures the same options twice, and the last callback registered wins for any setting both of them touch.

Properties

ApiPath

The path the API is served under. It is a property of the process, not of a scheduler: every scheduler is reached under this one path.

public string ApiPath { get; set; }

Property Value

string

Remarks

MapQuartzHttpApi(pattern) says the same thing where the endpoints are mapped, which is where the rest of an application's routes are written, and a pattern given there wins over this.

EventStreamHeartbeatInterval

How long GET {ApiPath}/schedulers/{name}/events may say nothing before it emits a Heartbeat frame. Fifteen seconds by default.

public TimeSpan EventStreamHeartbeatInterval { get; set; }

Property Value

TimeSpan

Remarks

A heartbeat is what keeps a stream that has nothing to report alive, and it is a setting rather than a constant because the thing it has to stay under is not Quartz's: a reverse proxy closes an idle connection after its own read timeout — nginx's proxy_read_timeout is 60 seconds, Azure's front doors 90, and a deployment is free to configure less. Set this below whatever sits between the API and its readers, and a stream that would have been cut every minute stays open.

It bounds silence rather than spacing the frames: a scheduler that is producing events sends no heartbeats at all, and the interval starts again after each frame. One goes out when the stream opens whatever this says, because a response's headers are not sent until its body is written.

IncludeStackTraceInProblemDetails

Whether a failure's stack trace is included in the problem details returned to the caller.

public bool IncludeStackTraceInProblemDetails { get; set; }

Property Value

bool

Remarks

It also puts the real message of a 500 back in the response body. Both are things to read while developing and neither is something to ship: a fault's message routinely names the server, the database, the login or the constraint that produced it.

IsJobTypeAllowed

Which job types a request may name. Null — the default — allows every one, which is what every earlier release did.

public Func<string, bool>? IsJobTypeAllowed { get; set; }

Property Value

Func<string, bool>

Remarks

This is the narrowing for the thing SECURITY.md says is not a vulnerability: a caller who passes authorization can schedule any type that implements IJob, and Quartz.Jobs' NativeJob implements it and starts the executable its job data names. An operator who knows which jobs their deployment schedules can say so here, and the surface stops being "any IJob on the probing path".

The predicate is given the job type name, exactly as the request spelled it. Nothing resolves it first, and that is the point: a name arriving over HTTP is data until the side that runs the job loads it, and resolving one here to compare types would be the assembly probe this API exists not to make. So match on the string — a set of names, a namespace prefix — and allow for the fact that one type has more than one spelling: Acme.Jobs.Nightly, Acme.Jobs and the same name with Version, Culture and PublicKeyToken after it are both it. A predicate matching a bare StartsWith on the namespace covers every spelling at once.

A refused name is 403 with problem details naming the type the request asked for and nothing else — it is the caller's own input, so it gives nothing away. The refusal is per request rather than per job, so schedule-jobs stores none of its batch when one job in it names a type that is not allowed.

It is one predicate for the process, like everything else here, so it cannot say that one scheduler may run a type another may not; the request's scheduler name is not passed to it. The dashboard's QuartzDashboardOptions.IsJobTypeAllowed is the same setting for that surface, and the two are configured separately because a deployment can map one of them and not the other.

MaxPageSize

The most items one paged request may return: 1000 by default, and 0 for no limit. A take naming a number above it is a 400.

public int MaxPageSize { get; set; }

Property Value

int

Remarks

A page size is the only thing on this API a caller can use to make the server do arbitrary work, and until 4.0.0-beta.1 nothing bounded it: one request could materialize every trigger in the store while the bulk key fetch next door refused 1001 keys. The default is that same 1000.

?take=all is bounded by this rather than refused by it. It does not name a number — it says "as many as you will give me" — so it is answered with this many, and hasMore says whether that was all of them. A listing whose matches fit under the cap therefore answers exactly as it would with no cap at all, which is what keeps the 3.x-compatible listings (GetJobKeys and its neighbours) working through HttpScheduler: they ask for everything whether the answer is three rows or three million.

Set it to 0 where an export or a migration really has to take everything in one call, and put the API behind something that says who may.

OperationAuthorizationPolicy

The authorization policy every operation is held to, evaluated against a SchedulerResource carrying the scheduler's name, the operation's name and the operator role it needs. Null — the default — leaves the API as it was: whoever reaches a route may call it. Added in 4.8.

public string? OperationAuthorizationPolicy { get; set; }

Property Value

string

Remarks

Name a policy built with RequireSchedulerRoles(…) and each route needs the role the route table gives it: View for every read, Operate for pausing, resuming, triggering and interrupting, Administer for adding, deleting, rescheduling and shutting down. A refusal is 403 with problem details naming the operation and the level, logged as event 9009.

The check runs after SchedulerAuthorizationPolicy's, before ReadOnly's and before the handler, so no body is read for a refused call. trigger is asked again at Administer once its body is read and carries job data. The scheduler listing is filtered to the schedulers the caller may view.

The two policies compose: SchedulerAuthorizationPolicy decides which schedulers a caller may reach, this one what they may do there, and ReadOnly stays the ceiling for everybody.

ReadOnly

Whether the API serves reads only. false by default, which is what every earlier release did.

public bool ReadOnly { get; set; }

Property Value

bool

Remarks

Set it and every route that changes something answers 403 with problem details saying the API is read-only, before the handler runs and therefore before a body is read or a scheduler is looked up. It is the whole mutating surface in one switch: an operator serving a dashboard or a monitoring tool out of a production process wants the reads and none of the writes, and getting that from route-level authorization means naming thirty-odd routes and keeping the list current.

What counts as mutating is a property of the route rather than of its verb. The two bulk fetches — POST …/jobs/fetch and POST …/triggers/fetch — are reads that take a body of keys, so they are served; everything else that is not a GET is refused. Pausing, resuming, interrupting and resetting a trigger from its error state all count: each of them changes what the scheduler will do next.

It is a bound on this API and on nothing else. The scheduler in the process goes on firing jobs, and a dashboard mapped beside it has its own QuartzDashboardOptions.ReadOnly — neither setting binds the other surface.

SchedulerAuthorizationPolicy

The authorization policy every route that names a scheduler is held to, evaluated against a SchedulerResource carrying that name. Null — the default — leaves the API as it was: whatever RequireAuthorization(…) the application put on the mapped group, applied uniformly to every scheduler.

public string? SchedulerAuthorizationPolicy { get; set; }

Property Value

string

Remarks

Set it and each request is checked with IAuthorizationService.AuthorizeAsync(user, new SchedulerResource(name), policy) before the scheduler is looked up, so a caller who fails cannot tell "no such scheduler" from "not yours": a refusal is 403 with problem details, and a 404 only ever answers a scheduler the caller was allowed to ask about. The scheduler listing is filtered the same way.

The check is authorization, never authentication: an anonymous caller gets whatever the policy says, which is a 403 when the policy refuses. Put RequireAuthorization() on the mapped group as well to have an anonymous caller challenged with a 401 first.