Changelog
What changed in each package, newest first. This page is made from the
CHANGELOG.md files in the repo when the docs are built.
dox_annotation
3.2.0
- New:
package:dox_annotation/install.dartwithInstaller, the steps of a<package>:installcommand thatdox addruns.writeFile(only when the file is not there),makeFolder,addEnv(to.envand.env.example, only when the key is missing),addEnvGetter(toAppEnvinlib/config/env.dart),addDependency(dart pub add, alsodev),buildRunner,note(add your own summary line) andprintSummary.linesis read only. Every step is safe to run twice. It is not in thedox_annotation.dartbarrel. - New:
DoxProvider.boot(). It runs once in every isolate aftersetup()of every provider is done. Optional, the default does nothing. dox_queue starts its in-process worker there, so the other providers (eg. the mail jobs) are ready first. - New:
package:dox_annotation/provider.dartwithDoxProvider. It moved here fromdox_core, so a package belowdox_corecan have a provider. It is not in thedox_annotation.dartbarrel. - New:
package:dox_annotation/command.dartwithCommandandMigrationFile. They moved here fromdox_core, so a package belowdox_corecan adddoxcommands. - New:
GlobalMiddleware, a small shared list that packages use to add global middleware at startup. A package that does not depend ondox_corecan use it.GlobalMiddleware.clear()is for tests.
3.1.3
- Version bump only, to match the other packages.
3.1.2
- Version bump only, to match the other packages.
3.1.1
- Version bump only, to match the other packages.
3.1.0
- New:
id()onIAuth, soreq.auth?.id()works. It gives the primary key of the logged-in user from theid()of the auth class (likeAuth().id()ofdox_auth), or null. It is an extension (IAuthHelpers), so a class that implementsIAuthdoes not need to change. For a guest,req.authis null.
3.0.2
- Removed:
ICacheDriverandIStorageDriver. Nothing used them. For a cache driver, useCacheDriverfromdox_cache. - Tests for
IHttpException.toResponse().
3.0.1
- Version bump only, to match the other packages.
3.0.0
- The first stable release.
3.0.0-rc.1
IDoxRequest.authis now a field (getter and setter), not only a getter. The auth is kept apart from the input.
3.0.0-alpha.17
- Version bump only, to match the other packages.
3.0.0-alpha.16
- Version bump only, to match the other packages.
3.0.0-alpha.15
- Version bump only, to match the other packages.
3.0.0-alpha.14
- New:
@Security.bearer(format: 'API key')and@Security.apiKey(header: 'X-API-Key'). Put it above a middleware class to show its login scheme in the OpenAPI docs.
3.0.0-alpha.13
- Version bump only, to match the other packages.
3.0.0-alpha.12
- Version bump only, to match the other packages.
3.0.0-alpha.11
- Version bump only, to match the other packages.
3.0.0-alpha.10
- Version bump only, to match the other packages.
3.0.0-alpha.9
- New:
@Inject(). Put it above a class anddox_builderadds the class to the Dox container.@Inject(singleton: false)makes a new instance each time.
3.0.0-alpha.8
- Version bump only, to match the other packages.
3.0.0-alpha.7
- Breaking:
IDox,IDoxWebsocketandWebsocketEventare removed. They were only used bydox_websocket.
3.0.0-alpha.6
- Version bump only, to match the other packages.
3.0.0-alpha.5
- New
timestampsoption on@DoxModel.timestamps: falseturns offcreated_atandupdated_at, like$timestamps = falsein Laravel.createdAt: nullorupdatedAt: nullturns off one of them. - New
primaryKeyTypeoption on@DoxModel. UseprimaryKeyType: Stringfor a UUID key. The default isint. - Fix:
IHttpException.toResponse()now returns{"code": errorCode, "message": message}for a text message, so the client gets theerrorCode. AMapmessage is returned as it is.
3.0.0-alpha.4
- Version bump only, to match the other packages.
3.0.0-alpha.3
- Version bump only, to match the other packages.
3.0.0-alpha.2
- Needs Dart 3.13 or newer.
- All dependencies updated to their latest versions.
- OpenAPI annotations, all optional:
@Summary,@Description,@Tags,@Request,@Body,@Query,@Response,@ApiDeprecated,@ApiIgnore,@DoxController. Lower case names work too (@summary(...)), except@Response. - The DTO annotations (
@Dto,@Length,@Email, ...) live indox_validation.dox_coreexports them. IDoxRequesthas novalidate()and noprocessInputMapper()any more. Use a DTO.- Breaking (small): these names are exported by
dox_coretoo. If a file also imports a package with aRequestorResponseclass (likepackage:http) without a prefix, add a prefix to that import. - New
@DoxEnv()and@EnvKey('X')for the typed env class.
3.0.0-alpha.1
- Version 3 of Dox. All Dox packages now share one version.
- Needs Dart 3.6 or newer.
- See the release notes for all main changes.
dox_builder
3.2.0
- New: the job builder moved here from dox_queue. It writes
lib/generated/jobs.g.dartwith every@Job()class, the same file as before. It runs only in an app that has dox_queue independenciesordev_dependencies. (When the build can not read the app'spubspec.yaml, it runs when the app can import dox_queue.) If yourbuild.yamlnames the builderdox_queue:jobs, change it todox_builder:jobs. - Breaking: this dox_builder needs dox_queue 3.2 or newer. With dox_queue 3.1, both packages have a builder for
jobs.g.dart, andbuild_runnerstops at start with "outputs collide". Rundart pub upgradeto move both together. An app that must stay on dox_queue 3.1 turns off the new builder, and the dox_queue 3.1 builder keeps making the file: - New: when the app can import dox_mailer,
jobs.g.dartalso lists the mail jobs ofMail.queue()(SendQueuedMailandSendEncryptedQueuedMail). So the worker can read them beforeMail.configure()runs. The saved job names do not change. A dox_mailer without these classes (3.1) gets no mail lines. - Finds
DoxProviderclasses fromdox_annotation. Release it together withdox_annotationanddox_core. An app must upgrade dox_builder together with dox_core, or its providers are not found. - New: the OpenAPI schema of a model reads
visibleFields, likehidden: with a list, only its keys are in the schema. Needs the next dox_orm fortoJson()to match. - New:
onTrashed()counts as a model hook, so a model with only that hook getshasModelHooks. Needs the next dox_orm. - New: models get
$copyColumnsFrom(). It copies every column field from another model of the same class.refresh()in the next dox_orm uses it, so it works for columns with a cast or abeforeGet. Nothing else calls it, so reads and saves do no extra work. - New: models read
@HasManyThroughand@HasOneThrough. Needs the next dox_orm. - New: DTOs read
@Dto(trim: true),@Dto(coerce: true)and@Dto(attributes: {...}). Needs the next dox_validation. - New:
@Afterand@Beforetake'today','tomorrow','yesterday'and the input key of anotherDateTimefield. New@AfterOrEqualand@BeforeOrEqual. A text that is not a date, a word or aDateTimefield stops the build. The words and fields work with any dox_validation.@AfterOrEqualand@BeforeOrEqualneed the next dox_validation. - New: DTOs read
@RequiredUnless,@RequiredWithout,@RequiredWithoutAll,@Filled,@Present,@Prohibited,@ProhibitedIf,@ExcludeIfand@ExcludeUnless. Each one on an always-required field stops the build, like@RequiredIf. Needs the next dox_validation. - New: the rules
@Differentand the field compare rules check the whole field on aListfield, not each item. Rules that name another key stop the build when they name a Dart field name in place of the input key. - A DTO that uses none of these gets the same code as before.
- OpenAPI: a controller method that calls
req.authorize(...)gets a401and a403answer in the docs. A@Responsewith the same status wins. - OpenAPI: a controller method that calls
req.validateSchema(...), orreq.validate<T>()with no request class found, gets a422answer. A@Response(422, ...)wins. Needs the next dox_core. - OpenAPI: a controller method that calls
Gate.authorize(...)gets a403answer in the docs. A@Response(403, ...)wins. - DTOs: a DTO with no
@RequiredIfor@RequiredWithfield (and no nested DTO with one) now writesusesRequiredWhen => false. Then the check does not keep each converted value, which saves work. Needs the next dox_validation. - New: DTO code reads the DTO flags of
ValidationFlagsat run time:dtoAllErrors,dtoRejectEmptyListanddtoMaybeRejectsEmpty. All are off by default, so the checks work as before. The builder writes this code only when the app's dox_validation hasDtoState.allErrors. With an older dox_validation it writes the old code, so it still compiles. - New: a model
@BelongsTofield passes its name asrelation:tobelongsTo(), for the dox_orm flagbelongsToKeyFromRelation. The builder writes it only when the app's dox_orm has therelation:parameter. With an older dox_orm the code does not change. - Fix:
@OrDefault()on aMaybe<T>field no longer stops the build. A bad value givesMaybe.missing(). - Fix: a nested DTO deeper in lists and maps, like
List<List<A>>orMap<String, List<A>>, now runs itsvalidate()and its async rules. Before, only one level (List<A>,Map<String, A>) was found. So afterbuild_runner, an app may now get errors from these deeper DTOs that were skipped before.
3.1.3
- Version bump only, to match the other packages.
3.1.2
- DTOs:
@Describe('...')on a field setsdescriptionin its JSON Schema (the generatedtoJsonSchema()and the OpenAPI docs). It wins over the///doc comment. Needs dox_validation 3.1.2.
3.1.1
- DTOs:
@Dto(emptyStringsToNull: true)makes the generated code read""asnull(needs the next dox_validation). - DTOs:
@OrDefault()makes a bad value of that field give its default (ornull) instead of an error. A field with@OrDefault()and no default that is not nullable stops the build.
3.1.0
- Fix: the JSON Schema pattern of
@AlphaDashnow allows digits, like the check does (needs dox_validation 3.1.0). The OpenAPI output of a DTO with@AlphaDashchanges. - Fix: in generated DTO code,
@RequiredIfand@RequiredWithnow run after all fields are converted. Before, a rule on a field read the raw value of a field that comes after it, like' PRO ', not'pro'after@Trimand@Lowercase. Runbuild_runnerto get the new code. - Fix:
reload()and preload now set a nullable@HasOne,@BelongsTo,@MorphOneor@MorphTofield to null when the related row is gone. Before, the field kept its old value. Runbuild_runnerto get the new code. - Fix:
reload()and preload now set a@HasMany,@ManyToManyor@MorphManyfield to an empty list when there are no related rows. Before, the field kept its old list. Runbuild_runnerto get the new code. - Behavior change: a nullable list relation, like
List<Tag>? tags, is now[](notnull) after a preload with no rows. - DTOs:
@RequiredIf('type', 'company')and@RequiredWith(['phone'])fromdox_validation. The field is required only when the condition is true. Use them on a nullable field, aMaybefield or a field with a default. On an always-required field the build stops with a clear error. - DTOs: the build stops when
@RequiredIf,@RequiredWith,@Sameor@Confirmed(other:)names a Dart field (likeaccountType) instead of its input key (account_type). The check would never match. It also stops for a@DateFormattedformat with a letter it does not know. - DTOs: in a
@Dto(strict: true)class, the key that@Confirmedreads (likepassword_confirmation) is not an unknown key. - New: DTO file rules
@MinSize('10KB'),@MimeTypes(['image/png', 'image/*'])and@Dimensions(minWidth:, maxWidth:, minHeight:, maxHeight:, width:, height:, ratio:). They need dox_validation 3.1.0 or newer. A bad size or a@Dimensions()with no limit stops the build. - New:
@ManyToMany(withPivot: [...], withTimestamps: true)is passed to the ORM, so the relation reads the extra pivot columns. Needs dox_orm 3.1.0 or newer. - New: the model builder makes the code for
@MorphMany,@MorphOneand@MorphTo(polymorphic relations). A@MorphTofield has no model: its type is the field type, likeModel<dynamic>, and its morph name is the field name in snake_case. The OpenAPI model schema has these relation keys too. Needs dox_orm 3.1.0 or newer.
3.0.2
- Breaking: column and JSON keys can change. Please read this before you update.
- Digits are no longer split off. Before,
address2gave the keyaddress_2. Now it givesaddress2. - A name you give is used exactly as written. Before,
@Column(name: 'userID')gaveuser_i_d. Now it givesuserID. The same is true for@Computed(name:), and forprimaryKey,createdAtandupdatedAtin@DoxModel. - DTO input keys change the same way: a field
line2now reads the keyline2, notline_2. - What to do: run
build_runnerand look for changed keys in the generated files. Then rename the column in a migration. Or keep the old key with@Column(name: 'address_2')(or@Name('line_2')on a DTO field). - Fix: types from an import with a prefix (
import 'other.dart' as o;) keep the prefix in the generated code, likeo.Role,List<o.Role>,EnumCast(o.Role.values)ando.$InnerDto. Before, the code did not compile. This works in models, DTOs, env classes and scopes. - Fix:
@Column(cast: ...)now works for a model in apartfile. The cast is read from the AST of the field. - Fix: table names, column keys and relation keys are escaped in the generated code (
\,'and$). - Fix: a private class (like
_AdminRouteror@Inject() class _Cache) is no longer put incontainer.g.dart, where it did not compile. The build shows a warning with the class and file. - Fix: container errors now show the file and line of the problem.
- New: a constructor parameter of an abstract type gets the one container class that implements it. When two or more classes implement it, the build stops and names them.
- Fix: the OpenAPI builder reads the files in path order, so
openapi.g.dartdoes not change between builds. - Fix: the OpenAPI builder finds routes with a prefixed dox_core import, like
dox.Route.get(...). Only theRouteclass of dox_core counts. - Fix: the OpenAPI builder stops with a clear error when two controllers, two
@Securityclasses or two schema classes have the same name in two files. Before, one silently replaced the other. Dox finds them by name at runtime, so a new name would not match. Rename one of them. - Fix: OpenAPI annotations count only when they come from a Dox package (
package:dox_...). An app class with the same name, likeSummary, is no longer read as one. - Faster builds: the container, DTO list and OpenAPI builders skip
.g.dartfiles. They also look at the text of a file first, and only resolve it when it can hold what they look for. -
Change: the model, DTO and env builder runs on
lib/only by default. Before, it also ran ontest/,bin/andtool/. A package with models or DTOs intest/adds this to itsbuild.yaml:
3.0.1
- Version bump only, to match the other packages.
3.0.0
- The first stable release.
3.0.0-rc.1
lib/generated/dtos.g.dartis now written for every app, also with no@Dto. The list is empty then. So the app can always havedtos: dtosinAppConfig. An app here is a package that importsAppConfig(from dox_core). Other packages with no DTO still get no file. The empty list isfinal, notconst, sodtos: dtosdoes not trigger theavoid_redundant_argument_valueslint.
3.0.0-alpha.17
- New: models get
setColumnValue(column, value). It sets a field by its column name, and returns false when the value is not of the field type.model.increment()andmodel.decrement()of dox_orm use it to set the field. The generated code needs the dox_orm that hasModel.setColumnValue(). Run build_runner again after you update.
3.0.0-alpha.16
- Version bump only, to match the other packages.
3.0.0-alpha.15
- Change: the container builder finds every class that extends
DoxProviderand writesc.provider<...>for it.
3.0.0-alpha.14
- New: the openapi builder reads
@Securityon middleware classes and writes them toOpenApiRegistry.security.
3.0.0-alpha.13
- Version bump only, to match the other packages.
3.0.0-alpha.12
- Fix: a route inside
Route.group('/web-auth', registerWebAuth)lost the group prefix when the callback was a method or function, not a closure. The builder now reads that method with the prefix, also for nested groups. The route call inopenapi.g.dartnow has the full path, like/web-auth/login.
3.0.0-alpha.11
- Version bump only, to match the other packages.
3.0.0-alpha.10
- The app-wide files are now written to
lib/generated/:lib/generated/container.g.dart,lib/generated/dtos.g.dartandlib/generated/openapi.g.dart. This keeps them out of the top oflib/. Import them frompackage:<app>/generated/.... Delete the old files inlib/after you rundart run build_runner build. - Model part files (like
user.model.g.dart) still sit next to their source file.
3.0.0-alpha.9
- New:
lib/container.g.dartbuilder. It finds every@Inject()class, every class that extendsRouter, every class that implementsDoxServiceand every@DoxModelinlib/. It writesregisterContainer(IocContainer c), which makes each class with its constructor. Constructor parameters come from the container by type. Optional parameters that the container can not give keep their default. Required nullable ones getnull. - The build stops with a clear message for a required parameter the container can not give, for a dependency cycle (it shows the chain, like
A -> B -> A), and for an abstract or generic@Inject()class. Two classes with the same name work: every import has a prefix. - OpenAPI: typed routes (
Route.get<C>(path, (C c) => c.method)) andRoute.resource(path, ControllerClass)are read too. - Generates the query scopes: an extension with one method for each
@Scope()method, andglobalScopesfor@Scope(global: true). - Generates the column casts:
fromMap()andcolumnsToMap()use thecastof@Column().booland enum fields get a cast withoutcast. - Run
dart run build_runner buildto get the new code.
3.0.0-alpha.8
- Version bump only, to match the other packages.
3.0.0-alpha.7
- The generated
fromMap()takes aDateTimefrom the driver as it is. It only parses a string. - New generated
columnsToMap()with the columns only.convertToMap()adds the relations to it. - Models with no
beforeGethook getreadsRowDates => true, so their rows skip the date to string step. AbeforeGethook still gets dates as strings. tableColumns,preloadList,timestampsColumnand the relation matchers areconst, so no new list or map is made on each call.- Run
dart run build_runner buildto get the new code.
3.0.0-alpha.6
- Version bump only, to match the other packages.
3.0.0-alpha.5
@DoxModel(timestamps: false)makes a model withoutcreatedAtandupdatedAt.createdAt: nullorupdatedAt: nullturns off only one.- The primary key field has the type of
@DoxModel(primaryKeyType: ...). It was alwaysint?. The default is stillint. The OpenAPI model schema uses the same type. - Fix: a preloaded relation with a two word name (like
userInfo) was missing fromtoJson(). The generator looked foruser_infoin the preload list, but the list hasuserInfo. - Fix: the OpenAPI route scanner now puts one
/between a group and a route path, like the router does.
3.0.0-alpha.4
- Version bump only, to match the other packages.
3.0.0-alpha.3
- Version bump only, to match the other packages.
3.0.0-alpha.2
- Needs Dart 3.13 or newer.
- All dependencies updated to their latest versions.
- New builder
dox_builder:openapi. It writeslib/openapi.g.dartwith the OpenAPI docs of the app: it reads theRoute.*calls, the controllers, the@Dtoclasses and the models. It writes nothing when the package has no controllers. DtoJsonSchema,ModelSchemaandJsonSchemaReader: the JSON Schema of a@Dtoclass, a model or any Dart type. Other builders can use them.- New: the DTO generator. For each
@Dtoclass it writes$<Name>Dto(aconst DtoSchema<Name>) into the.g.dartpart: plainifchecks per field, the converter,fromMap,toMapand the JSON Schema. It stops the build with a clear error for a bad DTO (a field that is not final, a type that is not supported, a rule that does not fit, a bad regex, ...). - New builder
dox_builder:dtos. It writeslib/dtos.g.dartwith every DTO of the app, forAppConfig(dtos: dtos). Two DTOs with the same name stop the build. DtoReader: the one place that knows DTO keys. The OpenAPI docs use the same keys as the DTO (snake_case by default,@Name,@Dto(keyCase:)).- OpenAPI:
@Regex(was@Pattern).@Formatis removed. Form requests are removed. - Fix: the OpenAPI route list has the full path, with the
Routerprefix andRoute.group()prefixes. Two'/'routes in two groups now each get their docs. - OpenAPI path params: every positional argument that is not the request and not a DTO.
- New:
@DoxEnv()classes get_$<Class>in the.g.dartpart. Each getter is read from the env once, converted to its type (String, int, double, num, bool, Uri, Duration, enum, List) and kept in a final field. A getter body is the default. A nullable getter is optional. load()throws oneEnvExceptionwith every missing or bad key.
3.0.0-alpha.1
- Version 3 of Dox. All Dox packages now share one version.
- Needs Dart 3.6 or newer.
- See the release notes for all main changes.
- Works with analyzer 5 and 6.
dox_cache
3.2.0
- New:
dox cache:install [--store=file|redis|memory|database], run bydox add cache. It writeslib/config/cache.dart, theCACHE_STOREkey and theAppEnvgetters, and addscache: cachetolib/config/app.dart. With--store=databaseit also addsdox_cache_database, its provider and the cache table migration. It never overwrites a file. dox_cache now depends on dox_annotation. - New:
'encrypt': trueon afileorredisstore encrypts the values. It is off by default, so nothing changes for stores without it. SetCacheConfig(encrypter: ...)with aCacheEncrypter. A store with'encrypt': trueand no encrypter throws aStateError, for every store type. Memory and null stores do not encrypt. Anencryptvalue that is nottrueorfalsethrows anArgumentError. - On Redis, a plain
intstays plain text when encryption is on, soincrementanddecrementstill work. Every other value is encrypted. To keep a secret number encrypted, store it as aString. - A value that cannot be decrypted (wrong key, changed text) reads as a miss. An old plain value reads as a miss too, except an
inton Redis. FileDriver.prune()deletes files that cannot be decrypted when encryption is on.- Cache keys and tag names are not encrypted, and a value is not tied to its key. Encryption protects against reading only.
maxValueBytesis checked on the plain JSON size. - Ships an AI agent skill (dox-cache-guide). Install it in your app with
dart run skills@ get. - New:
CacheFlags, opt-in behaviors that work like Laravel, asCacheConfig(flags: ...). All are off by default.CacheFlags.all()turns on every flag. - New:
CacheFlags.lockThrowsmakeslock.block(timeout)with no callback throwLockTimeoutExceptionwhen the time is over, not givefalse. - New:
CacheLock.throwOnTimeoutpicks this for one lock. A store sets it from the flag.CacheStoretakesflags:.
3.1.3
- Version bump only, to match the other packages.
3.1.2
- Version bump only, to match the other packages.
3.1.1
- New:
flexibletakes a nullstale. Then the value is kept until a refresh replaces it, so callers still get the last good value whencomputefails.
3.1.0
- New: the file driver can delete expired and broken files by itself. Set the
prune_everyoption (orFileDriver(pruneEvery: ...)) to runprune()in the background once every that many writes. It is off by default (0). Only one prune runs at a time, and a failed prune is ignored. - Fix:
FileDriver.prune()could delete a new value thatadd,incrementortouchwrote at the same time. Now it takes the same key lock as they do and reads the file again under it. A key that is locked right now is skipped.
3.0.2
- Fix:
rememberandrememberForeverthrew on a cache hit whenTwas not a raw JSON type, likeList<String>. Pass the newdecodeoption to turn the cached JSON intoT. Without it,Tmust be a raw JSON type. - Fix:
rememberrunscomputeonce when many calls for the same key run at the same time in one isolate. The others wait for the first. With the newlock: true, callers in other isolates and servers wait too. - Fix:
pullis atomic now. Redis usesGETDEL(needs Redis 6.2 or newer). Drivers get a newpullmethod. - Fix: the file driver keeps each prefix in its own sub folder.
flushdeletes only the files of its own prefix, and keeps temp files of writes that are running. - Change: because of this, files written by older versions are in the base folder and are not found any more. It is a cache, so they are only misses. You can delete the old files in the base folder by hand.
- Fix: a file driver read no longer deletes an expired or broken file. Another isolate may have just written a new value. Use the new
FileDriver.prune()to delete expired and broken files. - Fix: the file driver has locks now (
File.create(exclusive: true)).add,incrementandtouchtake a lock on the key, so they are safe across isolates. - Fix: a Redis prefix with no
:at the end gets one. So the prefixappdoes not match the keys ofapp2. - New:
flexible(key, fresh, stale, compute): stale while revalidate. - New:
touch(key, ttl)sets a new ttl and keeps the value. - New:
getandpulltake a function as the default. It runs only on a miss. - New:
lock.block(timeout, callback)runs the callback while it holds the lock. It throwsLockTimeoutExceptionwhen the time is over.lock.forceRelease()frees a lock of any owner. - New: the Redis option
max_value_bytes. - Faster: tagged
many,putMany,has,pullandrememberread the tag versions once per call. - Faster: the memory driver keeps the expiry as int milliseconds. It does not read the clock for items with no expiry.
hasdoes not decode the value. - Change:
MemoryEntry.expiresAtis anintin milliseconds now.CacheClockhas a newnowMs().
3.0.1
- Version bump only, to match the other packages.
3.0.0
- The first stable release.
3.0.0-rc.1
- Needs
ioredis3.0.1. It fixes a crash on disconnect when the Redis host is a name likelocalhostorredis.
3.0.0-alpha.17
- Version bump only, to match the other packages.
3.0.0-alpha.16
- Change: uses ioredis
^3.0.0, the first stable ioredis release.
3.0.0-alpha.15
- Version bump only, to match the other packages.
3.0.0-alpha.14
- Version bump only, to match the other packages.
3.0.0-alpha.13
- Version bump only, to match the other packages.
3.0.0-alpha.12
- Version bump only, to match the other packages.
3.0.0-alpha.11
- Version bump only, to match the other packages.
3.0.0-alpha.10
- Version bump only, to match the other packages.
3.0.0-alpha.9
- Version bump only, to match the other packages.
3.0.0-alpha.8
- Version bump only, to match the other packages.
3.0.0-alpha.7
- Perf: the memory driver removes expired items and expired locks in a sweep after many writes. Before, a key that was written once with a ttl and never read again stayed in memory forever.
- Perf: the memory driver with no
max_sizedoes not reorder the map on every hit. The order is only needed for LRU. - Perf: a tagged cache reads all tag versions with one
manycall (oneMGETon Redis).tags(...).flush()raises all tag versions at the same time. Before, each tag was one round trip after the other. - Perf: the Redis driver frees a lock with
EVALSHA. It sends the full script only when Redis does not know it yet. - Perf: the file driver makes the cache folder only when it is missing. Before, it ran
create(recursive: true)on every write. Lock tokens use one sharedRandom.secure().
3.0.0-alpha.6
- New
CacheManager.close(). It closes the connections of every store that was built. The Redis driver closes its connection and its pool connections, with no reconnect. Memory, file and null stores do nothing. It is safe to call twice. A store used after it is built again. - New
CacheDriver.close(), for one driver. Custom drivers can override it. The default does nothing.
3.0.0-alpha.5
- Version bump only, to match the other packages.
3.0.0-alpha.4
- Version bump only, to match the other packages.
3.0.0-alpha.3
- New:
getRaw(key)gives the stored value as JSON text, with no decode. Use it to send a cached value as a JSON response. Memory and Redis return the stored text as is. A stored String comes back with its quotes ("bar"). The stored format did not change, so old values still read the same. - Faster
get: a plain store builds the key with no async step.itemKeynow returnsFutureOr<String>. Memorygetcosts about 0.6 µs instead of 0.86 µs. MemorygetRawcosts about 0.3 µs. - New Redis option
pool_size: the most connections one store opens (ioredismaxConnection). Default is 10, the same as before. - Uses
ioredis2.0.0-alpha.2. It sends all commands on one shared connection and writes them together (auto-pipelining). A Redis read costs much less CPU. The cache now uses one connection per store, sopool_sizerarely matters.
3.0.0-alpha.2
- Needs Dart 3.13 or newer.
- All dependencies updated to their latest versions.
3.0.0-alpha.1
- Uses
ioredis2. Redis error replies throwRedisError. - Redis: no value size limit by default (was 60 KB).
maxValueBytesis still there if you want one. - Redis:
manyuses oneMGET,putManyuses one pipeline,flushusesSCAN+DEL(no Lua). - First release.
CacheManagerwith named stores and custom drivers.- Drivers: memory (with LRU), file, Redis, null.
remember,pull,add,increment,many, tags and atomic locks.
dox_core
3.2.0
- New:
Dox().isWebServer. It is true only in the web server process (startServer()and its isolates). It is false inbin/queue.dart, the scheduler and tests. A provider can use it to skip work that only a web server needs, like a startup warning. - New: after
setup()of every provider (and the routes), the app runsboot()of every provider (seeDoxProvider.boot()of dox_annotation). - Changed:
Command,MigrationFileandDoxProvidernow live in dox_annotation.package:dox_core/command.dartandpackage:dox_core/dox_core.dartstill export them. dox_core needs the dox_builder release that finds providers from dox_annotation, so upgrade dox_builder together with dox_core. - New:
RateLimit(sliding: true)uses a sliding window. A client can no longer send 2 xmaxrequests around the edge of a window. The count is the count of this window plus a part of the count of the window before. That part gets smaller as the window goes on.Retry-Afteris the time until one more request fits. Off by default: the fixed window works as before. - New:
CoreFlags.envExpand..envvalues can use${OTHER}, unquoted or in"..."(not in'...'). It reads a real environment variable first, then a key above it (.env.testingalso sees the keys of.env). An unknown name stays as it is. In a"..."value,\n,\t,\",\\and\$become a new line, a tab,",\and$. Off by default: values are read as before. The.envfile is read beforeAppConfigexists, so also setEnv.flags = flagsat the top ofmain(). The server isolates read.envthe same way as the main isolate.Dox().initializewarns when the flag is on inAppConfigbut.envwas read without it. - New:
export KEY=valuelines in.envare read likeKEY=value. - New:
AppConfig(htmlErrors: true)sends a small HTML error page to a request that does not want JSON (seeDoxRequest.expectsJson). Off by default, so errors stay JSON. Indevelopmentorlocalthe page also shows the error text and the stack trace. JSON clients, validation errors and answers fromExceptionHandler.renderdo not change.AppConfig(errorPage: ...)replaces the page;defaultErrorPageis the default. - New:
DoxRequest.expectsJsonFor(headers)isexpectsJson()for the headers of a rawHttpRequest. - New:
Route.resource(..., parameter: 'photo')names the param of one item{photo}instead of{id}, soRoute.bindand model binding work on resource routes. The default is still{id}. - New:
route()andRoute.url()put a param that is not in the path into the query string. Before, it threw anArgumentError. Whenparamsandqueryhave the same key,querywins. - New:
route(..., absolute: true)andRoute.url(..., absolute: true)give the full url, withAPP_URL(orbaseUrl:) in front, likeRoute.signedUrl. - New: a cache store with
'encrypt': trueencrypts its values with theAPP_KEY(Crypt). Old keys inpreviousKeyscan still read old entries. No setup is needed:CryptCacheEncrypteris the default. SetCacheConfig(encrypter: ...)to use your own.CacheEncrypteris exported from dox_core.CryptCacheEncrypter.encryptthrows aStateErrorwhenAPP_KEYis empty. - New:
Dox().addGlobalMiddleware(middleware). A provider calls it insetup()to add global middleware. They run on every route (and on mounts), before the app'sglobalMiddleware. The same middleware added twice runs once.Dox().allGlobalMiddlewaregives the full list. - Ships an AI agent skill (dox-core-guide). Install it in your app with
dart run skills@ get. - New:
Dox().loadRoutes()registers the routes of the app routers and gives them asAppRoutes(routes, fallbacks and mounts), without a server and without providers. When OpenAPI is on, the docs routes (/docs,/openapi.json,/openapi.yaml) are in it too. It is safe to call more than once: the route list is empty before and after the call, so a laterstartServer()starts clean.dox route:listuses it. - New:
RouteData.isWebSocketis true for a route added withRoute.websocket. - Fix:
Route.group,Route.domain,Route.middleware,Route.enabledCorsandRoute.disabledCorsrestore the prefix, name prefix, domain, middleware and CORS setting also when the callback throws. Before, the next routes got the wrong path, name or middleware. - New:
CoreFlags, opt-in behaviors that work like Laravel, asAppConfig(flags: ...). All are off by default, so nothing changes until you turn one on.CoreFlags.all()turns on every flag.Dox().flagsgives the flags of the current isolate. - New:
CoreFlags.laravelErrorssends a422as{"message": "...", "errors": {"field": ["..."]}}. The OpenAPIValidationErrorschema follows it. - New:
CoreFlags.onlyPresentmakesreq.only([...])leave out keys that are not in the input. - New:
CoreFlags.cookieDefaultssendsPath=/andSameSite=Laxfor aDoxCookiewith nopathorsameSite. - New:
CoreFlags.hasKeyOnlymakesreq.has('x')only check that the key is there, likereq.exists('x'). - New:
CoreFlags.htmlStringssends a string from a controller astext/html. A content type set by the app wins. - New:
res.header(k, v, replace: true)replaces a value that a middleware put on the response.CoreFlags.replaceHeadersmakes it the default.VaryandSet-Cookiestill get one more value with the flag. - New:
session.regenerate(keepToken: true)keeps the CSRF token, so forms open in other tabs keep working. The default is still a new token. Do not use it at login: a token from before the login would stay valid. - New:
CoreFlags.decryptServerErrormakes aDecryptExceptiona reported500. - New:
CoreFlags.cacheCopiesmakesCache().store('x'),.tag()and.tags()give a newCache. - New:
dox_coreexportsCacheFlagsandLockTimeoutExceptionofdox_cache.Cache.withAppPrefixkeeps theflagsof theCacheConfig. - Fix:
HealthCheck.cache('name')uses the result ofCache().store('name'), so it checks the named store withCoreFlags.cacheCopiestoo. - Change:
DecryptException.toString()givesDecryptException: The encrypted value is not valid, so a log line says what failed. - BREAKING: the JSON line of
Loggeruses the standard names of log services (Datadog).levelis nowstatus(lower case).nameis nowlogger.name(left out when empty). AMapin the data goes to the top level, other data goes underpayload. The standard fields can not be replaced by a data key. Pretty mode is the same. - BREAKING: the JSON line of
LogMiddlewarehas new field names.levelis nowstatus.source_ipis nownetwork.client.ip.request_idis nowhttp.request_id. New:http.methodandhttp.url.payload.requestandpayload.headersstay. Thefiltergets the new map. It can not changetimestamporlogger. Aloggerkey in the data is always ignored. - Change:
LogMiddlewarenow writes throughLogger. It followsLoggerConfig(enabled,level,prettyPrint). WithprettyPrintit prints the colored text line.enabledis now optional: not set follows the logger,falseturns only the request logs off. - Change:
Loggercollects lines and writes them in one write every 20 ms (or 64 KB), not oneprint()per line. Awarn,warning,dangerorerrorline is written at once, after the lines before it. The buffer is written when the app stops. - Fix: pretty
Loggerlines fordebug,warninganderrorhave a color. Before, they printednullin the color code. - New:
dox_coreexports theJSONhelper.JSON.stringifygives a String.JSON.bytesgives UTF-8 bytes in one pass.JSON.parsereads a String or UTF-8 bytes. Use bytes for sockets, files, queues and HTTP client bodies. Do not returnJSON.bytesfrom a controller. - OpenAPI: a route with the
Canmiddleware gets a401and a403answer in the docs. A route withGuestMiddlewareorValidateSignaturegets a403. They are found by class name. A@Responsewith the same status wins. - OpenAPI: the
ValidationErrorschema is in the docs whenever a route has the automatic422answer, also when it comes from areq.validateSchema(...)call. - New:
ServerTimingmiddleware. It adds aServer-Timingheader with thehandlerandtotaltime of the request. It is off unless you add it, because it shows timing to clients. Use it in development or for trusted clients. - New:
ServerTiming.add(name, duration)andServerTiming.measure(name, fn)add your own timing to the current request, with no request passed. The same name is summed and counted. Without the middleware they do nothing. The namestotalandhandlerare kept for the built-in times: your own ones are sent asapp_totalandapp_handler. AServer-Timingheader that the app sets itself is kept, and the times are added after it. - New:
AfterResponse.run(work)runs work after the answer of the current request is sent. Any code can call it, with no request passed. It is off untilAfterResponse.enable()(for example in thesetup()of a provider). Errors in the work are reported and never stop the server. - Fix:
Dox().stop()closes theIsolateBusagain after the providers shut down. Before, a provider that used the bus during its shutdown joined it again, and the open port could keep the process alive. - Docs:
IsolateBus.publishthrows for a value that can not be sent, before any isolate gets it. In the main isolate it throws only when another isolate listens on the bus.
3.1.3
- New:
IsolateBussends a message from one isolate to all the other isolates of the app, with no Redis.publish(channel, message)sends,on(channel)gets the messages of one channel,sizecounts the isolates that listen. The main isolate keeps the hub, like theRateLimitcounts. - New:
Dox().isolateCountis the number of isolates that really run, in every isolate. It also countsDox().totalIsolate(n)and the one isolate on Windows.
3.1.2
- Uses dox_validation 3.1.2, so
@Describe('...')can be used on DTO fields.
3.1.1
Cache().flexibletakes a nullstale: keep the last good value until a refresh replaces it.- New:
Hash.sha256(value)gives the SHA-256 hex of a value, to store API keys and access tokens as a hash (not for passwords). - New:
ValidationException.issueskeeps every issue of a failed DTO or schema check (path, rule code, message, params). AnExceptionHandlercan use it for another 422 shape, like[{field, message, rule}]. - New:
RequestBodyandBodyKindare exported, soreq.requestBody.kindcan tell invalid JSON (BodyKind.invalidJson) from no body, without importing a file oflib/http.
3.1.0
- Behavior change: without Redis,
RateLimitcounts are now shared by all isolates. Before, each isolate counted alone, so the real limit wasmaxtimes the number of isolates. Nowmaxholds for the whole server. An app with more than one isolate gets a lower real limit than before. Raisemaxif you relied on the old number. - Security: new check of the
Originheader on a WebSocket upgrade. UseRoute.websocket(allowedOrigins: [...])for one route, orAppConfig.webSocketAllowedOriginsfor all of them. It is off by default, so apps do not change. A WebSocket route with no check prints one warning on stderr at start. - Security fix:
back()only follows aRefererof the same host. A referer of another site goes to the fallback. Before, it was an open redirect. - Fix: a multipart body with no boundary or no
Content-Dispositiongets 400, not 500. - Fix: the headers of
abort()are kept on routes that have a next middleware. - Fix: a mounted handler that throws now gives 500. Before, the client could hang.
- Fix: a download name with quotes or non-ASCII letters now gets a safe
Content-Disposition. - New: one warning on stderr when CORS has origin
*together withcredentials: true. - New:
Crypt.encryptString/Crypt.decryptString(andCrypt.encrypt/Crypt.decryptfor JSON values) use theAPP_KEYof the app. Old keys inpreviousKeysstill decrypt. - New:
Hash.check, the same asHash.verify, with the Laravel name. - New:
Cache().many(keys)andCache().putMany(values, duration:)on the facade. The store already had them. - New:
MethodOverride(header: true)also reads theX-HTTP-Method-Overrideheader on a POST. It is off by default, so apps do not change. - Fix:
Route().setStatics(Route().statics)emptied the static folders. It cleared the list before it copied it. - Fix: after
%decoding, the.wherecheck now lets.match a new line. Before,.where('page', '.*')did not match/docs/a%0Abany more. req.pathIs(pattern)cleans each pattern once, not on every call.- Docs:
Route.viewis the WebDAVVIEWmethod. It does not render a template like LaravelRoute::view.Route.wheresays the value is checked again after decoding. - Security fix:
.wherenow checks the param again after%decoding. Before, it checked only the text as it is in the URL. So.where('name', '[^/]+')let/nos/..%2F..%2Fetc%2Fpasswdthrough, and the controller got../../etc/passwd. Now a decoded value that does not match the regex makes the route not match, like any other miss (the next route, the fallback or 404). Params with no%and routes without.wheredo not change. - Security fix:
req.pathIs()now cleans the path like the router does. Before, it only dropped the/at the start. So/admin/and/admin//reached the/adminroute, butpathIs('admin')was false, and a middleware guard on it was skipped. Now//counts as one/, and a/at the end does not matter, in the path and in the pattern. A path that matched before still matches. - Fix: with more than one isolate,
Route.staticFilesadded beforestartServer()and outside aRouterworked only in the main isolate. A file like/app.cssgot a 404 at random. Now the static folders go to every isolate, like routes and fallbacks. - Fix: a route with an optional param, like
/greet/{name?}, now shows in the OpenAPI docs as both/greetand/greet/{name}. Before, only/greet/{name}was there, because the?was removed beforedox_openapicould see it. - Fix:
req.session.reflash()now keeps the flash data when it is the first use of the session in a request. Before, it kept nothing, because the old flash keys were read before the session was loaded. - Docs:
hasdoes not read dot keys (onlyinput,filled,exists,missing,booleanandintegerdo).Route.redirectdoes not fill{id}in the target and drops the query string. A GET to a POST-only path gets the fallback if a fallback covers the path, else 405. A fallback added afterstartServer()does not reach the other isolates. - With
MethodOverride, a POST to a path with no route for any method gets 404 at once. Before, Dox read the whole body first. - Fix: with
MethodOverride, a POST with_method=PUTto a path with no PUT route now says "PUT is not allowed" in the 405 message. Before, it said "POST is not allowed". - The router calls
Route()once per route lookup, not twice. - The warning for a middleware that returns
nullreadsAPP_ENVonce, not on every such request. - Fix:
Crypt.decryptof a value made withCrypt.encryptString(text that is not JSON) gave a 500 error with aFormatException. Now it throwsDecryptException, a 400 error. - Fix: in the OpenAPI docs, the short path of an optional route, like
/greetfor/greet/{name?}, listed a 404 "Not found" answer. It has no param, so no record can be missing. Now only/greet/{name}lists the 404. - Fix: a
Route.fallbackin a group with a param in its prefix, like{tenant}, now matches, andreq.paramgives the param. Before, the prefix was compared as plain text, so the fallback never ran. - Fix:
.where('id', '^[0-9]+$')now matches. A leading^and a trailing$are dropped. Before, they ended up inside the route regex, so the route never matched. req.pathIs()andreq.routeIs()build the regex of a pattern once, not on every call.- Fix:
req.pathIs()gives false for a path with a bare%or a bad code like%zz. Before, it threw an error and the client got 500. req.boolean()no longer makes a new set of true words on every call.- New:
Route.match(methods, path, handler)adds one route for each method in the list.Route.any(path, handler)adds one for GET, POST, PUT, PATCH and DELETE. OPTIONS is left out, so the CORS preflight answer still works. - New:
Route.redirect(from, to, status:)andRoute.permanentRedirect(from, to). 302 and 301. - New:
.where(param, regex)on a route. The route matches only when the param text matches the regex. Else the next route is tried. Routes without it match the same as before. - New:
Route.fallback(handler)answers a GET or HEAD request that matches no route and no static file. It runs its middleware, so in a router the global middleware runs too. It follows the group prefix and domain. The built-in 404 and 405 do not change. - New: in development (
APP_ENVisdevelopmentorlocal), Dox prints a warning to stderr when a middleware returnsnull. That ends the request with an empty 200, and the controller does not run. Nothing else changes. - New request helpers on
DoxRequest: input(key, defaultValue)takes a default. A dot key likeuser.nameoritems.0.idreads inside maps and lists. A key that is in the input as it is still wins, so old code works the same.filled(key): the key is there and not empty.exists(key): the key is there, with any value.missing(key): the key is not there.hasdoes not change.except(keys),boolean(key),integer(key, defaultValue).file(key)andhasFile(key)for uploads.bearerToken()reads the token of anAuthorization: Bearerheader.path(),fullUrl(),pathIs(pattern)androuteIs(pattern). A*in the pattern matches any text. It ispathIs, notis, becauseisis a Dart keyword.- Docs: the
abortexample now usesreq.auth?.id().req.auth?.iddoes not exist. - New:
MethodOverridemiddleware. Add it toAppConfig.globalMiddleware, and a POST request with a_methodfield ofPUT,PATCHorDELETEgoes to the route of that method. So a plain HTML form can reachRoute.put,Route.patch,Route.deleteandRoute.resourceroutes.req.methodgives the new method.CsrfMiddlewarestill checks the request. Other_methodvalues and other request methods are ignored. It is off when the middleware is not there. - Fix:
res.header(name, value)now replaces a header with the same name in any case, likewithHeadersdoes. Before,header('Cache-Control', ..)aftercache()sent twoCache-Controlvalues. - Break: removed the unused
middlewareAndControllerHandlerandgetDoxRequesthelpers. - Fix:
res.withHeaders(map)now adds the headers to the ones set before it. Before, it dropped them all, also theContent-Dispositionof a download. A key in the map replaces the header with the same name, in any case (cache-controlandCache-Controlare one header). - New:
SessionMiddleware().fromCookie(value)loads a session from its cookie value, with no request. Use it for a Socket.IO login. Give the value as the browser sent it (still encrypted). It runs the same checks as the middleware: a changed cookie, a cookie of another name, a cookie made with another APP_KEY, an expired session or an unknown session id gives an empty session. Changes to it are not saved. - New:
DoxCookie.decrypt(name, value)decrypts a cookie value likereq.cookie(name)does.req.cookienow uses it, with no change in what it accepts. - New:
package:dox_core/command.dartwith theCommandbase class. A package or an app adds its owndoxcommands with it. Name a library with a top-levelcommandslist in thedox: commands:key of pubspec.yaml, and thedoxCLI finds it. It is a separate library, so it does not clash with otherCommandclasses (like the one ofpackage:args). - New:
MigrationFileinpackage:dox_core/command.dart. A command writes a migration intodb/migrationwith it, with the same file name asdox make:migration. It does not write a second migration with the same name. - Fix:
Route.mountmatches whole path parts only. A mount at/adminnow gets/admin,/admin/and/admin/..., but no longer/administratoror/admin-login. Only requests that went to the wrong handler change. A prefix that ends in/(like Socket.IO's/socket.io/) works as before. - Fix:
//counts as one/when a path is matched to a mount prefix. So/admin//secure/xgoes to a mount at/admin/secure. The handler still gets the path as it came. - Behavior change: when two mount prefixes match, the longest one wins. Before, the first one added won. Apps with one mount, or with mounts that do not overlap, do not change.
- New:
Route.mount(prefix, handler, middleware: true)runsAppConfig.globalMiddleware(security headers, CSRF, rate limit) before the raw handler. A middleware that gives a response stops the request.RequestTimeoutdoes not run for a mount. When two mount prefixes match, the longest one wins.terminatesteps run once the answer is sent. The global CORS config does not apply to a mount. The default is still no middleware, so Socket.IO does not change. RequestFile.headnow keeps the first 128 KB of a big upload (on disk), and all bytes of a small upload (in memory, no copy). Before, it kept 32 bytes. A JPEG keeps its image size after the photo metadata, so@Dimensionsandv.file().dimensions()need these bytes.- New:
Route.staticFiles(prefix, folder, maxAge:)serves a folder (CSS, JS, images) for GET and HEAD requests that match no route. Routes always win..., dot files and folders get 404. Files getLast-Modified,304, Range support, the global CORS headers and an optionalCache-Control. The prefix followsRoute.group, the router prefix andRoute.domain. - New:
Route.staticFilessends a weakETag(from the file size and change time). A matchingIf-None-Matchgets304with no body.If-None-Matchwins overIf-Modified-Since. - New:
Route.staticFilessends files in a.well-knownfolder at the top of the folder (ACME challenges,security.txt). Every other dot name, also inside.well-known, still gets404. - Fix: a file response with
If-Rangenow uses theRangeonly whenIf-Rangeis the file'sLast-Modifieddate or its strongETag. Else it sends the whole file with200, so a resumed download does not mix two versions of a file. - Fix: a
Range,If-Modified-SinceorIf-None-Matchheader sent on two lines no longer gives500. - Fix: a static file
304has the file's content type, nottext/plain. - New:
AppConfig.idleTimeout. A keep-alive connection with no new request for this long is closed. Default 30 seconds, as before. Behind a load balancer, set it longer than the balancer's idle timeout (eg. 65 seconds for AWS ALB). Needs dox_http_native 3.0.3. - New:
AppConfig.maxConnections. The most connections one isolate holds open at once. Must be 1 or more (0 or less throws anArgumentErrorwhen the server starts). When it is full, new clients wait in the OS listen queue until a connection closes. WebSocket connections and connections that are still closing count too. The queue holds up to 1024 clients, and the OS can make it smaller (somaxconn; 128 by default on macOS). When the queue is full, new clients wait and then time out. On Linux each new client goes to one isolate (SO_REUSEPORT), so it can wait for a full isolate even when others have room. Default no limit, as before. Needs dox_http_native 3.0.3. - New:
Route.websocket(maxMessageSize:). A bigger message closes the socket with 1009. Default 16 MB, as before.
3.0.2
- Change: a Redis cache store with no
prefixgets<app name>_cache:fromAPP_NAME(likemy_shop_cache:), so two apps on one Redis do not share keys or flush each other's cache. Existing Redis cache keys are not found once after the update. Rate limit counters, cache locks andcachedriver sessions start fresh, so users with acachedriver session log in again. Set'prefix': 'dox_cache:'on the store to keep the old keys. - New: the
Cache()facade hasflexibleandtouch, andremember/rememberForevertakedecodeandlock. - Perf: the route binder does not load a value again when a middleware already bound it, like
Can(param: 'post')of dox_auth. - New:
RequestIdmiddleware. Each request gets an id in theX-Request-Idheader and inreq.requestId. A safe id from the client is kept.LogMiddlewarelines haverequest_id, andExceptionHandler.reportcan readExceptionHandler.requestId. - New:
ETagmiddleware. GET and HEAD answers with a body in memory get a weakETag. A matchingIf-None-Matchgets304 Not Modifiedwith no body. - New: signed urls.
Route.signedUrl(name, params:, query:, expires:, absolute:, baseUrl:)adds asignature(HMAC-SHA256 with thedox:signed-urlkey) and an optionalexpires. TheValidateSignaturemiddleware answers403for a bad or expired url. Old keys fromAppConfig.previousKeysstill work. - New:
RequestTimeout(Duration)middleware andRoute.get(...).timeout(Duration). A slow route answers503with the coderequest_timeout. The late result is dropped. - New:
IDoxTerminableMiddleware. Itsterminate(req, res)runs after the answer is sent. An error in it is reported, and never changes the answer. Routes without one do not pay for it. - Fix: a middleware item with no
handlestep at the start of a route list ended the request with an empty answer. It is now skipped.
3.0.1
- Perf:
AESEncryptor(encrypted cookies, the cookie session, CSRF) now runs AES-GCM in Rust (AesGcmof dox_http_native 3.0.2), with the AES instructions of the CPU. One session cookie took about 116 µs to encrypt and 95 µs to decrypt, now about 1.7 µs and 0.9 µs. The output is the same, so cookies made before still open. - Removed the
pointycastledependency. HKDF now uses HMAC-SHA256 ofpackage:crypto(from the Dart team). The keys are the same. - New: a path that has routes, but not for the request method, answers
405 Method Not Allowedwith anAllowheader (likeAllow: GET, POST, HEAD). Before, it was404. - New:
abort(code, [message, headers])andHttpStatusException(code)for any status. Common codes get a defaulterrorCodeandmessage. Theheadersare sent with the answer. - New:
ForbiddenHttpException(403) andMethodNotAllowedHttpException(405). - New: bracket keys in form bodies and query strings nest, like in Laravel.
tags[]=a&tags[]=bgivesreq.input('tags') == ['a', 'b'], andaddress[city]=xgives a map. A repeated key with no brackets still keeps its last value. - New:
AppConfig(previousKeys: [...]). Encrypted cookies and cookie sessions made with an oldAPP_KEYcan still be read, so a new key does not log out every user.AESEncryptor.decodehas a newpreviousSecretsoption. - Fix: path params are now URL-decoded.
/users/a%20bgives'a b', not'a%20b'. A URL fromRoute.url()now gives back the same value. A param that is not valid UTF-8 (like%ff) gives 404. - Fix: 5xx HTTP exceptions (like
InternalErrorExceptionandQueryException) are now sent toExceptionHandler.report. Before, everyIHttpExceptionwas skipped, so real server errors never reached the logs or Sentry. 4xx exceptions are still not reported. - Fix:
req.headersjoins repeated header values with,. Before, twoAcceptvalues became one string with no separator. - Fix: a
Rangeheader with a number too big for an int gave 200 with an empty body. A start that is too big now gives 416, and a suffix that is too big gives the whole file. - Fix:
req.methodis now the request method. A HEAD request on a GET route saidGET. - Fix: on a route with no params, a write to
req.paramstayed for the next requests to that route. Each request now has its own map.
3.0.0
- The first stable release.
3.0.0-rc.1
- Breaking: Dox no longer uses
APP_KEYas it is. It makes one key per purpose from it with HKDF-SHA256 (RFC 5869):dox:cookiefor cookies and the cookie session,dox:encryptforAESEncryptor, and more. See the newKeyDerivationclass. Existing encrypted cookies, sessions and tokens become invalid after this version. Users must log in again. AESEncryptor.encodeanddecodetake apurposelabel. The default isdox:encrypt. The secret can now have any length.- Fix:
Hash.makeandHash.makeAsyncwith a password over 72 bytes threw the bcrypt error, and its text held the password. So the password could end up in the logs. Dox now throws its ownArgumentErrorwith no password in the text. - New:
Hash.equals(a, b)compares two secrets in constant time.CsrfMiddlewareuses it. - Fix: the
cookiesession driver did not check the save time in the cookie. A copied cookie stayed valid untilAPP_KEYchanged. A session older thanlifetimeis now a new empty session. - Breaking:
req.authis now its own field on the request, not a value in the input. Before, a query or body fielddox_authentication_instancemadereq.auththrow a 500.AUTH_REQUEST_KEYis removed. UseAuth().setUser(req, user)or setreq.auth. - Fix:
APP_ENV=testingstopped the app on a badAPP_KEY. It is now treated liketest: only a warning. Loggeris now exported frompackage:dox_core/dox_core.dart.- The HTTP exceptions (
NotFoundHttpExceptionand the others) take adynamic message, likeIHttpException. A map message is sent as JSON as it is, likeNotFoundHttpException(message: {'error': 'not_found'}). - Security: the server now sends
X-XSS-Protection: 0, not1; mode=block. OWASP says the old browser filter should be off.SecurityHeadershas a newxssProtectionoption. Anulloption (or anullvalue inextra) now removes the header, also a server default likeX-Frame-Options. - Security:
CsrfMiddlewareno longer reads_tokenfrom the query string. It reads only the body and theX-CSRF-TOKEN/X-XSRF-TOKENheaders. - Doc comments: fixed the examples of
Cache.remember,Route.domain,Route.middlewareandRoute.disabledCors.
3.0.0-alpha.17
- Version bump only, to match the other packages.
3.0.0-alpha.16
- Version bump only, to match the other packages.
3.0.0-alpha.15
- Change: the startup hook is
DoxProvider. Writeclass X extends DoxProvider.setup()runs once in every isolate at start.shutdown()is optional and runs once in every isolate when the app stops. List providers inAppConfig.providers, or add them withDox().addProvider()andDox().addProviders(). The container method isc.provider<T>().
3.0.0-alpha.14
- Fix:
AUTH_REQUEST_KEYis now only indox_core, so importingdox_coreanddox_authtogether no longer gives an "ambiguous import" error. - Fix:
response(x).header('Content-Type', 'image/svg+xml')failed with "More than one value for header content-type". The defaulttext/plainstayed next to the new type. Nowheader()replaces the content type, the same ascontentType(). The name can be in any case. - Fix: when sending the error answer failed too, the client got no answer and waited until the timeout. Now Dox reports the error and sends a plain 500. When the head is already sent, it closes the response.
- New:
.env.testing. When the app runs its tests (dart test, or a realAPP_ENV=testing), Dox reads.env.testingon top of.env. Its keys win over.env. A real environment variable still wins over both. It works forEnv.get()and the typedAppEnv.load(). Use it for a test database:DB_NAME=my_app_test. NewEnv.isTesting, andEnv().load(testing: true). - New:
DoxTestClient.withSession()gives a test client that works like a browser. It keeps the cookies of every response, removes deleted or expired ones, and sends theXSRF-TOKENcookie back inX-XSRF-TOKENonPOST,PUT,PATCHandDELETE. It does not follow redirects.client.cookiesgives the kept cookies. The normal client does not change. - Fix:
OpenApiConfig(enabled: null)now treats an emptyAPP_ENVasdevelopment, the same default asappEnvof the typed env. Before,/docswas off whenAPP_ENVwas not set (for example in CI). SetAPP_ENV=productionin production. - Change: an empty
APP_ENVmeans production everywhere in Dox:/docsis off, the 500 error text is hidden and a badAPP_KEYstops the app. SetAPP_ENV=developmentin your.envand in CI. TheappEnvdefault of the typed env in the docs and examples is nowproductiontoo (wasdevelopment). - New:
@Securityon a middleware class sets the login scheme of its routes in the OpenAPI docs.OpenApiConfig.securityByMiddlewarestill wins over it. - Change: a middleware class name that ends with
AuthMiddleware(and has no@Security) now gets abearerAuthscheme with nobearerFormat. Before, the docs called every such token a JWT.
3.0.0-alpha.13
- New:
OpenApiConfig(include: ['/api/*'], exclude: ['/api/internal/*'])picks which routes go in the OpenAPI docs by path. A*at the end matches the rest of the path.
3.0.0-alpha.12
- Breaking:
dox_coreno longer depends ondox_storageanddox_socketio. Add the package you use to your app. - Storage:
Storage(),StorageConfigandStorageDiskFactorymoved todox_storage.AppConfig.storageis removed: extendStorageServicefromdox_storage(class AppStorageService extends StorageService { AppStorageService() : super(storageConfig); }).Route.files()is nowStorage.route().Dox().storageis nowStorage.manager.RequestFile.store()needsimport 'package:dox_storage/dox_storage.dart'. - Socket.IO:
SocketIoConfigmoved todox_socketio.AppConfig.socketIois removed: extendSocketIoServicefromdox_socketio.Dox().socketIois nowSocketIoService.server. dox_coreno longer exportsDisk,DriveManager,FileNotFoundExceptionandStorageException. Import them fromdox_storage.StreamFileandDownloadableFilestay indox_core.- Fix: the OpenAPI docs could give a route the docs of another controller with the same method name. The docs now pass the controller class of
(C c) => c.methodandRoute.get<C>routes to the registry. - New:
ControllerAction.controllergives the controller class.
3.0.0-alpha.11
- New: middleware that runs after the controller. Implement
IDoxNextMiddleware(handle(DoxRequest req, Next next)), or use a function(DoxRequest req, Next next).await next()runs the rest of the middleware and the controller and gives aDoxResponse, so the code after it can change the status, headers and cookies. A thrown error comes back as its error response, so the after part still runs. Before-only middleware work as before. A route with nonextmiddleware takes the same fast path as before. - New:
DoxResponse.statusandDoxResponse.headersgetters. - New:
redirect(url, status: 302)andback(req, fallback: '/')helpers. - New: sessions.
SessionMiddlewaregives each requestreq.sessionwithget,put,has,forget,all,flush,pull,flash,reflash,keep,regenerate,invalidateandtoken.AppConfig(session: SessionConfig(...))picks thecookiedriver (all data in a cookie, encrypted with AES-GCM, 4 KB limit) or thecachedriver (a random id in the cookie, the data in adox_cachestore likeredis). The cookie isHttpOnlyandSameSite=Laxby default. The cookie driver reads the session on first use. The session is saved, and the cookie sent, only when it changed. - New:
CsrfMiddleware.POST,PUT,PATCHandDELETErequests must send the session token in_token,X-CSRF-TOKENorX-XSRF-TOKEN, else419(TokenMismatchException). It sets anXSRF-TOKENcookie that axios and Inertia send back for you.except:skips paths like/api/*. - New: a validation error on a request with a session that does not want JSON (a form post, an Inertia request) flashes
errorsand the old input (never password fields), and redirects back. JSON requests still get422. Read them withreq.session.errorsandreq.session.old('email').req.session.flashErrors(...)does the same by hand. Newreq.expectsJson()andreq.hasSession. - New:
DoxCookie(sameSite: SameSite.lax). - New:
TestResponse.cookiesandDoxTestClient.withCookies(...), to go on with a session in tests.
3.0.0-alpha.10
- Version bump only, to match the other packages.
3.0.0-alpha.9
- New: a real IoC container.
IocContainer(Dox().ioc) hasget<T>(),has<T>(),singleton<T>(),factory<T>(),instance<T>(),override<T>()andclearOverrides(). Each isolate has its own container: a singleton is one instance per isolate, made on first use. - New:
AppConfig(container: registerContainer)with the function fromlib/container.g.dart(made bydox_builder). Every isolate runs it. The routers and services it finds are registered for you, soAppConfig.routersandAppConfig.servicescan stay empty. The ones you list there still work and come first. - New: typed routes.
Route.get<UserController>('users', (UserController c) => c.index)(also post, put, patch, delete and the others). The controller comes from the container, once per isolate, at start.(UserController c) => c.indexwithout<UserController>works too.Route.resource('users', UserController)takes a controller class. - New: a controller parameter whose type is in the container gets it from the container.
- New: route model binding for
@DoxModelclasses. A controller parameter of typeUsergets theUserof the{user}param (or the next param), found by its primary key. No row answers 404.Route.bindstill wins. - Breaking: the old
IocContainermethodsregister,registerByName,registerSingletonandgetByNameare removed. Usefactory,singletonandget. - New: named routes.
Route.get('/users/{id}', c).name('users.show'), thenroute('users.show', params: {'id': 5})gives/users/5.query:adds a query string.Route.group(..., name: 'admin.')puts a prefix in front of the names inside. Resource routes are namedblogs.index...blogs.destroy;Route.resource(..., name:)picks another base name. A name used twice stops the app at start. An unknown name, a missing param or an extra param throws anArgumentError. - New: route model binding without
dart:mirrors.Route.bind<User>('user', (String id) => User().find(id))runs before the controller of every route with a{user}param. The controller gets it as aUser userparameter, or withreq.bound<User>('user'). A binder that givesnullanswers 404. - New:
ExceptionHandler, the one place that turns an error into a response. Extend it and setAppConfig(exceptionHandler: ...).report(error, stackTrace)logs,render(error, req)picks the response. The default answers are the same as before. A URL with no route goes through it too. - Breaking:
AppConfig.errorHandleranddefaultErrorHandlerare removed. OverrideExceptionHandler.reportinstead. It gets the same errors: every error that is not anIHttpException, also errors outside a request. A thrown value that is not anExceptionorError(like aString) is now reported too. responseHandlerstill works and still runs afterrender.- New:
RateLimitmiddleware.RateLimit(max: 60, per: Duration(minutes: 1), key: (DoxRequest req) => req.ip()). One request too many gets429withRetry-After. Every answer hasX-RateLimit-LimitandX-RateLimit-Remaining. The counts live in Redis when the default cache store is Redis (shared by all isolates and servers), else in memory. Fixed window. NewTooManyRequestsException. - New:
SecurityHeadersmiddleware. It addsX-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-PolicyandCross-Origin-Opener-Policy, andStrict-Transport-Securityon https requests only. Each one can be changed or turned off withnull.Content-Security-Policyis off by default. - New:
Route.health('/health', checks: {...}). It answers 200{"status": "ok"}, or 503 when a check throws, returnsfalseor times out.HealthCheck.cache()checks the cache. The error text is in the answer only indevelopmentandlocal. - New:
package:dox_core/testing.dartwithDoxTestClient.DoxTestClient.start(appConfig)runs the app in the test on a free port.get,post,put,patch,delete,send,withHeadersandwithToken.TestResponsehasstatus,body,json,headersandexpectStatus,expectJson,expectBody,expectHeader. - dox_core now depends on
matcher(for theexpectmethods ofTestResponse).
3.0.0-alpha.8
- Uses dox_http_native 3.0.0-alpha.8: pipelined requests are about 2.8 times faster (1 core: 172k to 480k req/s). Normal requests are as fast as before.
- Security: every file response (
File,Storage().stream(),Storage().download(),Route.files) sendsX-Content-Type-Options: nosniff. Before, aFileresponse and a raw download did not. - New:
Route.files(path, download: true)sends every file as a download (Content-Disposition: attachment). Use it for user uploads. html, svg, xml and javascript files are still always sent as a download in a sandbox. - Fix: a trusted proxy
X-Forwarded-Forhop with a port (1.2.3.4:5678,[::1]:5678) is now read without the port. Before, it was not a valid ip, soreq.ip()could be wrong. A bare IPv6 like::1still works.
3.0.0-alpha.7
- Breaking:
dox_websocketis gone. Usedox_socketiofor realtime.Dox().websocketandDox().setWebsocket()are removed. - Breaking:
Route.websocket(path, handler)is now a raw WebSocket route.handlergets the openWebSocketand theDoxRequest. Middleware runs first. A request that is not a WebSocket upgrade gets 400. dox_http_nativenow lives in its own repo (dartondox/dox-http-native). Dox uses it from pub.dev (^3.0.0-alpha.7). Its build hook downloads a ready-built library, so you do not need Rust.- Perf: faster route matching. A plain route is found with one map lookup, also when a param route was added before it (as long as that param route can not match the same path). Param routes are grouped by their first path segment, so a request only tries the routes that can match. The first matching route still wins.
- Perf: the request handler reads
HostandContent-Typeonce per request.Hostis read only when the app has aRoute.domain()route.HttpBody.read()takes an optionalcontentType. - Perf: on the native server, a JSON or form body is used as it is, with no stream in between. The size limit is still checked.
- Perf: JSON responses are encoded in 8 KB chunks, not 256 bytes.
- Perf: fewer objects per request.
RouteData.params,DoxRequest.paramandDoxRequest.methodare made on first use.RouteData.domainLoweris made once. CORS header lists are joined once. - Perf: one Future less per async request (
then(onError:)instead ofthen().onError()). - Perf: multipart parsing makes its regexes once, and no longer builds a
Future.anyper chunk. - Perf:
LogMiddlewarelowercases theredactwords once, not per key. - Perf: an error response reads
APP_ENVand lowercases it only when the value changed. - Perf:
IocContainercallsT.toString()once per call. - Breaking: the
dart:ioserver is gone. Every app runs on the native server (dox_http_native), also an app with WebSocket or Socket.IO routes.AppConfig.server, theServerBackendenum and theDOX_SERVERenv var are removed.DoxServer.usesNative()and theisolateIdoption ofDoxServer.listen()are removed. - Breaking: when the native library does not load, Dox stops at start with an error. It says why (eg. an ABI mismatch) and how to fix it (install Rust so the build hook can build it, or rebuild your
lib_pathlibrary). Before, Dox logged a warning and useddart:io. - On a graceful stop (SIGTERM,
Dox().stop()), open WebSockets get close code 1001. - Perf:
AESEncryptorkeeps the key bytes of the last secret and writes the output into one buffer. Decode reads the nonce and text without copies. The output format is the same. - New:
Hash.makeAsync()andHash.verifyAsync(). They run bcrypt in another isolate, so a request does not block the server. - New:
Hash.rounds(default 10) sets the bcrypt cost.Hash.make()also takesrounds:. - Security: the app does not start when
APP_KEYis empty, not exactly 32 characters, or one of the old public sample keys from.env.example. With such a key anyone can sign tokens and cookies. InAPP_ENVdevelopment,localortestDox only logs a warning. A longer key (eg. 48 or 64 characters) is refused too, because AES needs a key of 16, 24 or 32 bytes and every encrypted cookie failed with it. Before, only an empty key inproductionstopped the app. Rundox key:generatefor a new key. - Security: a multipart part header with many backslashes (
name="\\\\...) no longer blocks a server isolate for hours (ReDoS in the quoted string regex). - Security: a
StreamFileresponse (Storage().stream(),Route.files()) with an html, xml, svg or javascript type is sent withContent-Disposition: attachmentandContent-Security-Policy: sandbox. EveryStreamFilegetsX-Content-Type-Options: nosniff. Before, an uploadedx.htmlorx.svgwas shown inline, and its script ran on the app origin (stored XSS). - Security: a
Route.domain()route only matches when theHostheader matches. Before, a request with noHostmatched every domain route. AHostwith a port (admin.app.com:8080) now matchesRoute.domain('admin.app.com'). - Security: an encrypted cookie is bound to its name (AES-GCM associated data). A value made for
localegivesnullwhen it is sent asuser_id.AESEncryptor.encode()/decode()take a newassociatedDataoption. Encrypted cookies made by older versions no longer decrypt, soreq.cookie()givesnullfor them once. Users may need to log in again. - Security:
DoxCookieisHttpOnlyby default.secureisnullby default, which setsSecurewhen the request came over https (req.scheme(), seetrustedProxies). SethttpOnly: falseorsecure: falseto change it. - Security: the native server checks
maxBodySizeandmaxMultipartSizeon their own. Before, Rust let any body up to the bigger limit (100 MB) through, so a JSON body could take 100 MB of RAM before Dart said 413. - New
AppConfig.bodyTimeout(default 120 seconds). On the native server, a client that does not send the whole body in this time gets 408. It stops slow clients that hold memory and connections.
3.0.0-alpha.6
- Security:
Storage().putRequestFile()andRequestFile.store()no longer trust the client file name when the bytes are a known format that does not match it. Before, a valid png sent asa.htmlpassed@Image/@Mimesand was saved as.html, soRoute.files()served it astext/htmlon the app origin (stored XSS). Now it is saved with the extension of the bytes (.png). - Security: new upload file names use a random uuid (v4), not a time-based one (v1). A v1 name shows the upload time and the server, and the next name is easy to guess.
- Security:
trustedProxies: ['*']trusts the direct peer only.req.ip()is the right-mostX-Forwarded-Forhop, the one the proxy added. Before,*also trusted every hop in the header, soreq.ip()was the left-most value, which the client sets (X-Forwarded-For: 6.6.6.6). That broke IP rate limits and allow lists, also behind the Fly.io proxy of the deploy guide. - Security:
LogMiddlewarewrites[REDACTED]for body fields and headers whose name haspassword,passwd,secret,token,authorization,cookie,api_key,api-keyorapikeyin it (upper or lower case). Before, the login body (with the password) and, withwithHeader: true, theAuthorizationheader went to the log as plain text. Change the list withLogMiddleware(redact: [...]). - Security: an error that is not an
IHttpExceptionanswers500with{"code": "server_error", "message": "Server Error"}. The error text is sent only whenAPP_ENVisdevelopmentorlocal. Before, the client always goterror.toString(), which can hold SQL, table names, file paths or database hosts.errorHandlerstill gets the full error. - Graceful shutdown closes the cache stores of
Cache()(eg. Redis), in every isolate, after theDoxShutdownservices. Your app needs no extra code. - The
ORMServiceof a new project implementsDoxShutdownand callsSqlQueryBuilder.close(), so the database connections close on shutdown too. In an older project, add this tolib/services/orm_service.dart:
3.0.0-alpha.5
- Fix: CORS
origincan be a list. Dox compares the requestOriginwith the list and sends back only the origin that matches, or no header. Before, it joined the list with commas, and browsers rejected it. A list also sendsVary: Origin. - Breaking:
AESEncryptornow uses AES-GCM with a random nonce for each message. Before, it used AES-CTR with an all-zero nonce and no message check. Old encrypted cookies and values can not be read. - Breaking:
AESEncryptor.decodethrowsDecryptException(400) when the text was changed or the key is wrong. Before, it printed an error and returned''. req.cookie()returnsnullfor an encrypted cookie that can not be decrypted.- Fix:
Hash.verifyreturnsfalsefor a malformed hash. Before, it threw. AESEncryptorandDecryptExceptionare now exported frompackage:dox_core/dox_core.dart.- Fix: in
.env,KEY=value # notegave the valuevalue # note. A#after a space now starts a comment. A#inside quotes, or with no space before it, is kept. - Fix: the
.envparser removed every quote character inside a value. Now only the quotes around the value are removed. - Fix:
HEADrequests got 404, alsoHEAD /docs. AHEADrequest now runs theGETroute of the same path. The answer has the status and headers ofGET, with no body. - New: Dox listens on
PORTwhenAPP_PORTis not set. Cloud Run, Fly, Render and Heroku setPORT.APP_PORTstill wins. - New:
AppConfig.hostand the env varAPP_HOSTpick the network interface to listen on, like127.0.0.1. The default is the same as before: all interfaces (::, IPv6 and IPv4). - Fix: the start log always said
http://127.0.0.1:<port>. It now shows the real host and port, likehttp://[::]:3000. - Removed the unused
sprintfdependency. - New: graceful shutdown. On
SIGTERMorSIGINTevery isolate stops taking new connections and lets open requests finish, up toAppConfig.shutdownTimeout(default 8 seconds). Then Dox runsDoxShutdownservices and exits with code 0. Before, open requests were cut. A second signal stops at once. - New:
DoxShutdown. A service that also implements it getsshutdown()in every isolate on stop, to close connections. - New:
await Dox().stop()stops the server gracefully in every isolate, without exit. - Fix: an
s3storage disk withoutbucket,keyorsecretfailed withNull is not a subtype of String. Now it fails with a clear message that names the missing setting. - Fix: a URL with no route now answers like a thrown
NotFoundHttpException. The body is JSON ({"code": "not_found", "message": "GET /x not found"}), and the appresponseHandlerruns for it, so apps can format it. - Fix: the error answer of an
IHttpExceptionnow has itserrorCode. A text message gives{"code": "<errorCode>", "message": "<text>"}as JSON. Before, the body was plain text anderrorCodewas never sent. AMapmessage (like validation errors) is still sent as it is. - Fix: the
Loggernow readsLoggerConfig.level. Lines below that level are not printed. Levels from low to high:debug,info(andsuccess),warn,error(anddanger). - Fix:
Route.group('blogs')withRoute.get('all')made/blogsall. Path parts now join with one/. This also works for nested groups,Route.prefixandRoute.resource. - Fix:
Route.resourcehid every error from the controller. Now it only skips methods the controller does not have.
3.0.0-alpha.4
- Version bump only, to match the other packages.
3.0.0-alpha.3
- New:
Cache().getRaw(key)gives the cached value as JSON text, with no decode. Send it as a JSON response with no extra encode step. - New Redis cache option
pool_size(seedox_cache).
3.0.0-alpha.2
- New default server:
dox_http_native(Rust hyper parses HTTP, your code stays in Dart). Hello world goes from about 41k to about 144k req/s on one isolate. Setserver: ServerBackend.dartIoinAppConfig(orDOX_SERVER=dartio) to usedart:io. Dox usesdart:ioby itself, with one warning line, when the native library does not load, and for apps with WebSocket or Socket.IO routes. - Faster requests: a simple
GEThello world went from about 31k to about 40k req/s on one isolate (plaindart:iois about 46k). A request with no body and sync middleware and controller now runs with noFuture.req.ip(), the query,req.all()and cookies are read on first use only. Reading the client ip was the biggest cost, because it asks the socket. Clean paths skip the two regex calls, and plain routes are found with a map lookup. - Fix: a
@Dtobody with aRequestFilefield shows asmultipart/form-datain the OpenAPI docs. - Fix:
req.ip()gave::ffff:127.0.0.1for IPv4 clients. Now it gives127.0.0.1. The same goes forX-Forwarded-Forhops. - Needs Dart 3.13 or newer.
- All dependencies updated to their latest versions.
- Removed the
encryptpackage (not maintained, and it blockedpointycastle4). AES for cookies and the file cache now usespointycastle4. The output is the same, so old cookies and cache files still decode. - Fix:
Env.get()of a missing key with no default gave the text"null". A missingAPP_KEYbecame the key"null", which anyone can guess. NowStringgives'',String?givesnull, andint/num/double/boolthrow a clear error. New:Env.get<bool>(). - Startup stops with an error when
APP_KEYis empty andAPP_ENV=production. Other environments log a warning. - New: typed input with DTOs. A controller asks for a
@Dtoclass by type:store(CreateUser body). Dox reads the body (or the query forGET), checks it, and answers422with{"items.1.quantity": "..."}when it is wrong.await req.validate<T>()does the same inside a method. See the Validation guide. - New:
AppConfig(dtos: dtos)(fromlib/dtos.g.dart) andAppConfig(validationMessages:)for other languages.req.localeis the firstAccept-Language. - New: typed controller parameters.
DoxRequestanywhere, one@Dto, andString,int,double,num,boolpath params by position. A path param that does not fit its type answers404. The types are read once at start; an unknown type stops the server with a clear error. - New: optional path params
{id?}. - New: query strings and form data with repeated keys (
tags=a&tags=b) and bracket keys (address[city],items[0][name]) for DTOs. - A DTO answers
400for a body that is not valid JSON, and415for a body type it can not read. RequestFileimplementsFileInputand keeps its first bytes, so@Mimesand@Imagecheck the real file type.- Breaking: removed
req.validate(Map),req.validateAsync(),FormRequest,AppConfig(formRequests:),DoxValidator,IocContainer.registerRequest()andprocessInputMapper(). Use a DTO.req.validateSchema(schema)stays for input you only know at runtime. - Breaking: a controller with more path params than the route has
{...}params stops the start. Before, the extra ones got''. - Fix: a typed path param like
int idgave a 500 (it was always aString). - A
ValidationExceptionfromdox_validation(for exampleschema.parse()in a controller) answers 422. dox_coreexportsdox_validation(not itsValidationException), with the DTO annotations.dox_ormalso hasSchema, andpackage:uuidhasUuid: usehideor a prefix in a file that needs both.- Breaking:
Cache()runs ondox_cache.AppConfig.cacheis adox_cacheCacheConfig(defaultStore:, stores:)with option maps.CacheDriverInterfaceandFileCacheDriverare removed. Values must be JSON types.putwithout a duration keeps the value forever (was 1 hour). New:add,pull,increment,decrement,remember,rememberForever,lock,tags. - Breaking:
Storage()runs ondox_storage.AppConfig.fileStorageis nowAppConfig.storage(StorageConfig(defaultDisk:, disks:)with option maps forlocal,s3,memory).StorageDriverInterfaceandLocalStorageDriverare removed. An unknown disk name throws. Storage().stream()anddownload()stream from any disk (S3 too), withContent-Length. New:url,signedUrl,use()for the fullDisk.- New
Route.files('files')serves local signed URLs (403 for a bad signature). New catch-all route param{path*}. - OpenAPI:
AppConfig(openApi: OpenApiConfig(...))adds/docs,/openapi.jsonand/openapi.yaml. On by default only whenAPP_ENVis development or local. Bodies come from the routebody:option or a DTO.Dox().writeOpenApi(dir)writes the files. RouteData.preMiddlewarenow holds the group and router middleware.- Socket.IO:
AppConfig(socketIo: SocketIoConfig(enabled: true, ...))serves/socket.io/on the app port, before the router.Route.mount(prefix, handler)is the raw hook. Off by default. - No extra work per request when OpenAPI or Socket.IO is off.
- OpenAPI docs are automatic.
dart run build_runner buildwriteslib/openapi.g.dart. Pass it asOpenApiConfig(registry: openApiRegistry). Each route then gets its summary and description (from the///comment), tags,Class.methodoperation id, typed path params, request (a@Dtoclass) and answer schema (from the return type), plus401,404,422and500. - A middleware whose class name ends with
AuthMiddlewareaddsbearerAuthto the docs, with no config. - A
GETorDELETErequest class becomes query params in the docs. dox_coreexportsOpenApi,OpenApiRegistry,OpenApiOperation,OpenApiRouteBindingandResponseDocofdox_openapi.- The OpenAPI JSON and YAML are encoded once and kept, not on each request.
- New
EnvReaderandEnvExceptionfor the typed env class (@DoxEnv()). The real environment wins over.env, the same asEnv.get().Env.get()stays for dynamic keys. - OpenAPI:
/docsuses Scalar by default.OpenApiConfig(ui: DocsUi.swagger)gives Swagger UI. NewOpenApiConfigoptions:scalar: ScalarOptions(...)(theme, dark mode, download button, proxy) andpageTitle.
3.0.0-alpha.1
- Version 3 of Dox. All Dox packages now share one version.
- Needs Dart 3.6 or newer.
- See the release notes for all main changes.
- Faster: streamed bodies, all CPU cores, faster router.
uuid4 andmime2.