Problem/Motivation
JSON API sister issue of #1927648: Allow creation of file entities from binary data via REST requests.
Per https://github.com/json-api/json-api/issues/246, the JSON API spec has not yet prescribed how this should be handled. That issue has been open for nearly 4 years. Last feedback from the spec maintainers on Feb 24, 2017. They're open to make a formal recommendation in the spec based on input. I think we can relay Drupal's experience implementing #1927648 in Drupal core and the trade-offs that were considered there. See https://github.com/json-api/json-api/issues/246#issuecomment-282192434.
Proposed resolution
Flow 1 (most cases)
Request:
POST /jsonapi/entity_type/bundle/{uuid}/file_field
Content-Type: application/octet-stream
Accept: application/vnd.api+json
// binary data
Response:
200 /jsonapi/entity_type/bundle/{uuid}/file_field
Content-Type: application/vnd.api+json
// file entity
Flow 2 (creating a new entity with a required file field)
Request:
POST /jsonapi/entity_type/bundle/file_field
Content-Type: application/octet-stream
Accept: application/vnd.api+json
// binary data
Response:
200 /jsonapi/entity_type/bundle/file_field
Content-Type: application/vnd.api+json
// file entity
2nd Request:
POST /jsonapi/entity_type/bundle
Content-Type: application/vnd.api+json
{
// data with relationship to newly created file entity
}
Remaining tasks
Port core's test coverage. Done, see #74. This covers "Flow #2".
Test coverage for "Flow #1"
Prove that the shim works in core's rest.module too
User interface changes
None.
API changes
None.
Data model changes
None.
Comments
Comment #2
wim leersThis will certainly be a 2.x-only feature.
Comment #3
gabesulliceI think this should actually live in a separate module, not JSON API itself.
I have a 2 proposed flows. The first takes advantage of JSON API and its concept of
relationshipsroutes which gets us a DX improvement over core REST, the second is a fallback for the case of required file fields which takes us back to core REST DX. Both can live side-by-side.For an existing resource:
Request:
Response:
The above would only work for a resource which already exists and has an image field. The advantage over core REST with the above is that we can automatically upload the file, create the file entity and reference the file entity all in one request!
This of course will not work for creating resources with a required file field. In that case, I propose to fall back to the following flow:
Request:
Response:
2nd Request:
Comment #4
wim leersMaybe, but not convinced about that just yet.
😍😍😍😍😍😍😍😍
Alternative proposal: make it impossible to make file fields required instead 🙈 j/k of course…
This does not work because it doesn't allow for validating the file before uploading it.
Comment #5
gabesulliceThis would be yet another extension to the spec that we'd be putting in the module without a negotiation or advertisement mechanism. That's why I said it should be separate. I would be willing to be convinced otherwise.
Darn! It was so elegant. I think we're still fine though.
/jsonapi/file/filebecomes/jsonapi/entity_type/bundle/file_field. The spec is silent about URLs, so this is okay.Comment #6
gabesulliceUpdated the IS with the proposal.
Comment #7
gabesulliceUpdating the 1st flow paths from
/jsonapi/entity_type/bundle/relationships/file_fieldto/jsonapi/entity_type/bundle/file_fieldbecause the spec enforces that the documents to and from relationships must contain only resource identifiers not resource objects.Comment #8
wim leersYou've convinced me :) Once it's in the spec (or in a spec), we could deprecate that module and eventually make it available by default in
jsonapiitself.Thanks for that excellent issue summary!
Comment #9
ebeyrent commentedFYI, there's already the JSON API File module: https://www.drupal.org/project/jsonapi_file
Might be a good chance to pull in the code from that module and deprecate it.
Comment #10
wim leersThanks for making that connection :)
Unfortunately that module only allows base64-encoded uploads and requires the https://www.drupal.org/project/file_entity module. It cannot be reused as-is though: it needs a significant rework to not rely on base64 encoding. It's going to be much easier to start from #1927648: Allow creation of file entities from binary data via REST requests.
We're talking to the maintainer about this! :)
Comment #11
wim leersNow that the 2.x branch of JSON API has actually started, and we're trying to get JSON API into core, it'd be great if we could get this done too.
Comment #12
e0ipso+1
Comment #13
malik.kotob commentedShould this be postponed until #2940383: [META] Unify file upload logic of REST and JSON:API lands, which is now active?
Comment #14
gabesulliceAs much as that makes sense, I don't trust the core process to get that service in, in time. So, I'd be fine if this issue remained open and we created our own, temporary service.
Comment #15
malik.kotob commented@gabesullice so JSON API would create its own temporary service, and simply swap it out for the one #2940383: [META] Unify file upload logic of REST and JSON:API provides, once it is completed? @e0ipso or @gabesullice, do we know what kind of timeline JSON API is working with to try to close this out? Trying to make a decision on how to proceed with supporting remote file upload for a client site since both #2983404: [PP-1] RESTs FileUploadResource plugin is only able to check `create` access to a parent entity, should be able to check `edit` also and #2940383: [META] Unify file upload logic of REST and JSON:API are outstanding. If there are no plans to fix this issue in the next few weeks, I can either:
1. Attempt to write a custom rest plugin of my own that essentially copies what was done in #2940383: [META] Unify file upload logic of REST and JSON:API, but takes in the entity as a parameter. This may not be a very big lift, but would duplicate core's code.
2. Leverage the
jsonapi_filemodule. My understanding is that this route is not desirable since that module will become obsolete once the current issue is fixed, and it introduces a dependency on thefile_entitymodule.What're your thoughts on the best route to take?
Comment #16
gabesulliceThat's my hope!
This is high on my list, and @Wim Leers' too... but you could make it higher! If you post a patch, I can almost guarantee you'll have reviews from either @Wim Leers or I within a business day :)
I would say that you should do something like #1, but instead, make the service we need out of the existing plugin :P If this isn't committed in time for you, you can still pretty trivially make a REST plugin of your own that will wrap it. You'll be no worse off.
The only way I think that this doesn't land in JSON API within a few weeks is if there's a specification incompatibility that we can't work around. But in THAT case, we'll just move the code to a contrib module (maybe
jsonapi_file2.x), and you'll still be good.So, starting that service is definitely the best way forward.
Comment #17
malik.kotob commentedAlrighty @gabesullice, this patch introduces the temporary service to JSON API but doesn't actually create the necessary endpoints. It might be quicker for you or @Wim Leers to take on that part as opposed to providing me guidance on how to do it. Feel free to provide feedback on what should change with the service.
P.S. Since it doesn't actually introduce the JSON API endpoints, I actually hacked core's file upload rest resource plugin to point to this service to test it and was able to upload a file successfully.
Comment #18
gabesulliceThanks @malik.kotob! This is a great start!
Here's my very rough, "first reaction" review:
I could be wrong about lots of things below :P
Let's mark this service as "private" with:
public: falseWe should probably change this from "Handler" to something else. "Handler" has a specific meaning WRT to entities.
Not sure what yet though.
I wonder if we should add a namespace like
Drupal\jsonapi\Shim.Needs docblock and to be marked
@internalPerhaps we need an interface? That could be added later though.
I think this method would be more useful if it accepted a stream
like the one
fopen()returns.Then
handleUploadForFieldwould becomehandleUploadForField($entity_type_id, $bundle, $field_name, $filename, $stream)What do we need to do to this service to handle the case of an existing parent entity?
Comment #19
e0ipsoI love seeing progress on this issue!
Comment #20
malik.kotob commented1. Done
2. I changed it to
FileUploadService, what do you think?3. Done (I think that makes sense)
4. Done (with
FileUploadServiceInterfacecreated as well)5. Done (not sure if I implemented it as you envisioned though)
6. I was brainstorming a couple of solutions here:
a) add the entity as a parameter, if it’s present, check access at the field level (that doesn’t change), and also check access against the entity via
EntityAccessControlHandler::access(), passing along the entity and the operation (update), if it’s not present, continue to check access usingEntityAccessControlHandler::createAccess()and the field-level checkb) add a parameter that specifies whether or not to perform an access check, if the value is true, we’ll do the check as is, if its not true (which would be how jsonapi/rest/graphql are more likely to use it), they can perform the access check themselves. The resource specific to files being created against new entities would likely do the access check against
createAccess, and the resource specific to files being created against existing entities would do the check againstaccessWhat do you think?
Comment #21
malik.kotob commentedHere's a patch with some fix ups after I tested (again, using core's file upload rest resource plugin). I also went ahead and incorporated option 6a here just to get something going. With it, I was able to successfully upload a file against the current user entity.
Comment #22
gabesulliceThis is awesome :)
Some initial thoughts:
Let's make this a lot simpler, I think it can just receive
FieldDefinitionInterface $field_definition, $filename, $streamwith no optional params.JSON API, REST and GraphQL can do all the figuring out WRT to which field definition and how to check access.
This is just a file upload handler after all.
Let's rename this to something like
readFileData($stream)(no optional param).Instead of the optional parameter, let's add a new public static method on the service:
getUploadStream()which returnsfopen('php://input', 'rb').I went ahead and took those thoughts and put them into code.
Mainly, I wanted the file uploader service to just have a single method that does just what it's named. Convenience functions for access checking and validation etc are not on the interface and are public+static, so they can be used even if someone swaps out the upload handler (that should make it easier to handle uploads to s3, for instance).
Let me know what you think.
More thoughts:
We might want to get rid of HTTP-specific exceptions. For example, we could change
UnprocessableEntityHttpExceptioninto anInvalidArgumentException(since the $file argument failed validation). If this is a generic file upload handler, then I think we need to let JSON API/REST/GraphQL map those exceptions into whatever makes sense in context. I'm not convinced of that though.The more I think about this, the more I think it belongs in a submodule, not the main module.
Comment #23
gabesulliceI didn't like the owner of the file being optional, I don't think that makes much sense.
One thing I didn't document and is not clear in the previous interdiff is that I removed calls to
fclose($file_data). Since we're passing that in, I think it's the responsibility of the caller to close the stream when it's done with it.Comment #24
malik.kotob commentedAwesome work @gabesullice!
RE #22:
1. This makes sense to me, good call
2. Makes sense
3. Makes sense
This actually makes sense to me, but it seems like it may be a good fit to perform that refactoring in #2940383: [META] Unify file upload logic of REST and JSON:API.
RE #23:
I agree on both of these, both good catches!
Separately, I found a minor leftover "optional" comment in the doc block for a parameter on a function. Went ahead and a attached a new patch that has that fix.
How do we move forward from here? I think what's left is:
1. Deciding if this needs to get moved into another module
2. Adding in the two jsonapi endpoints that actually invoke this service
Is there anything else?
Comment #25
gabesulliceGood catch!
1. Given #3 + #5 + #8, which I had forgotten about, I think this needs to be in a submodule.
2. #3 + #5 describes what I think would be the ideal set of routes for this. @malik.kotob do you want to try that, or would you rather @Wim Leers or I take that on?
Not that I can think of off the top of my head.
I'm working on #2953346: Define related/relationship routes per field, not dynamically (with route parameters that need validating) ATM. That will change how relationship field access is handled and will make access checking here even easier. So I think we can do everything except access handling for now.
Comment #26
malik.kotob commentedOkay great, @gabesullice! If you're able to provide some guidance/best practices on how to define the routes and make it compliant with the spec, I can take a crack at this.
Comment #27
gabesulliceWRT to spec compliance, there's not a lot to do... because the spec doesn't define anything about file uploads :P
But, #3 mentions a few request and response headers that I think we should use. Those are pretty close in spirit to the spec.
For the response to the upload request, after you've created/saved the file entity in code, I'd make a subrequest for the file entity, then just update the
selflink in the response.To get started, I'd look at this module's
jsonapi.routing.ymlandsrc/Routing/Routes.php. Just look at theroutesandgetEntryPointRoutemethods to begin with (ignore line 107).If you get started with the most basic thing, we can build it up from there.
Comment #28
malik.kotob commentedThis patch is very very very rough :) It does two things:
1) Pulls out the code from previous patches into a submodule
2) Introduces the new route for existing resources mentioned in #3.
I tested that the file uploader service was still in good shape after I finished step 1, but don't actually have the route code working. I'm posting this work-in-progress because my bandwidth is limited at the moment, so I'm leaving it here in the event that someone else can pick it up and run with it.
Comment #29
malik.kotob commentedAlrighty, here's another rough patch, but with more progress.
Highlights:
- Added the controller for file upload requests for file uploads to existing entities
- Added a service provider to register 'application/octet-stream' as a known (bin) format. Core does this in 8.6.x, but we probably can't/don't want to add in that dependency here.
Outstanding:
- Add the routes and corresponding controller code for files being uploaded to new entities.
- Add in the code to handle subrequests so that a file being uploaded to an existing entity can be taken care of in one fell swoop (I guess this doesn't have to be MVP, but it's up to you all!)
- Address any comments from a future review :D
Comment #30
gabesulliceTHIS IS AMAZING PROGRESS! Nice work!
Let's move this to a
modulesdirectoryI try to avoid the word "endpoint"
"Provides JSON API file upload support"
I don't think this is necessary.
Nit: Let's just do this in one line.
You'll need to check
$access_result instanceof AccessResultReasonInterface.The Go language has a
deferkeyword which I always miss for this kind of thing.😘👌
Itty bitty nit: add
@internalLet's not worry about these assertions, we're already doing it in the main module.
This method looks perfect 👍
Nit: root vs base vs prefix
As per above, this should be defined to be
/{$base_path}/ {{$entity_type_id}}/{filed_field_name}(so, missing
relationships)As of #2953346: Define related/relationship routes per field, not dynamically (with route parameters that need validating), JSON API has its own custom relationship field access check that we can use :)
So we can add:
$individual_file_upload_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);Let's move these to
routes()Comment #31
malik.kotob commentedOkay, I've addressed the above feedback and completed the outstanding items mentioned in #29, with the exception of the subrequests feature. Gabe and I discussed and feel that one is not necessarily MVP and can be postponed to a future ticket.
This patch now provides 2 routes, one that accepts file upload requests given an existing entity to upload to, and one that accepts requests without the existing entity (with the understanding that a new entity will be created). The route for existing entities is defined at:
/jsonapi/entity_type/bundle/{uuid}/{file_field_name}. The route for new entities is defined at:/jsonapi/entity_type/bundle/{file_field_name}.Comment #32
malik.kotob commentedSorry for the noise, but now that this issue doesn't address the subrequest portion, that means we're no longer technically updating an entity with a file. We're now only creating new file entities. As such, I've attached a new patch that updates the method on the route that accepts an existing entity to
POSTfromPATCH.Comment #33
gabesulliceJust moving the module to a
modulesdirectory. No other changes.Comment #34
gabesullice@malik.kotob realized and discussed the fact that we need to do entity validation all over the place with me and suggested moving that out to a trait. Done.
Assigning to myself for a few more changes to come.
EDIT: ignore this interdiff, it has uncommitted changes in it.
Comment #35
gabesulliceProper interdiff for #34.
Comment #36
gabesulliceJust code standards and cleanup.
Comment #37
gabesulliceHere's my though process and explanation for the attached changes.
P.S. I realize my above interdiffs result in broken code. They're mostly split up in a way that's easy to see what was being changed. This patch should make it all workable again.
I think next up is a set of good tests.
Instead of using the same controller method for both new and existing resources, let's make dedicated ones.
Let's do the access checking before anything else.
Let's make this message a little more helpful to the user.
Let's rename these to
new_resource_file_upload_routeandexisting_resource_file_upload_route.Let's also rename these using that "new" and "existing" language.
A cool trick we can do here, since this is new development, is name the parameter "entity" and not vary it per entity type.
That will let us add it as a method argument in the controller.
Comment #38
gabesulliceRebased.
Comment #39
gabesulliceAdding subrequests and fixed an entity validation issue. Also, a variable name typo.
Comment #40
gabesullice1 new CS violation and a leftover line.
Comment #41
malik.kotob commentedAwesome job, @gabesullice!! I tested the patch from #40 on user entities with success. I haven't had a chance to code review yet, but as you mentioned in chat earlier, this could really use a review from @Wim Leers and/or @e0ipso.
Comment #42
wim leers@malik.kotob+++++++++++++++++++++++ for getting this going again! 👏 👌
@gabesullice++ for providing timely feedback, which I obviously gloriously failed at.
Sounds good — #2929932: Work around core's ill-designed @FieldType-level TimestampItemNormalizer normalization until #2926508 lands did something similar: it added
Drupal\jsonapi\ForwardCompatibility. Perhaps that should've been namedShiminstead?Could you share your thought process?
I can 😇 😁 … Test coverage! It was super super hard to get #1927648: Allow creation of file entities from binary data via REST requests right, and an essential piece was its test coverage. Without test coverage, this is extremely risky to add, because it may even result in one being able to overwrite files, upload new executable PHP code, and so on.
I'll review the patch, but without considering every tiny little detail that went into #1927648: Allow creation of file entities from binary data via REST requests. In fact, in this first review, I'll stay mostly at the architectural level.
(Repeating myself, sorry.)
Why a separate module?
This is
ForwardCompatibilityagain, because in 8.6\Drupal\file\FileServiceProviderwill ship which makes this obsolete.Hence we also want a
@todoindicating this should be removed once JSON API requires >=8.6.👍
How are these URLs discoverable? AFAICS file upload routes are never linked from anywhere? (I grepped for
.file_upload., 0 results. Perhaps that's not the right way to search?)This is basically a first attempt at doing #2983404: [PP-1] RESTs FileUploadResource plugin is only able to check `create` access to a parent entity, should be able to check `edit` also. In essence, it lifts a reusable component out of an existing implementation.
It'd be super valuable to validate this lifting by posting a patch to #2983404: [PP-1] RESTs FileUploadResource plugin is only able to check `create` access to a parent entity, should be able to check `edit` also that adds this service to Drupal core, refactors
\Drupal\file\Plugin\rest\resource\FileUploadResourceto instead call this service (or perhaps it should be a trait?), and try to make it pass tests.If it still passes tests, then that would give us a lot of confidence, and would make it much easier to commit this patch :)
Why have an interface?
This signature looks great!
These can easily be moved to a separate issue. (It's out of scope, and would make this patch a bit smaller and thus easier to review and commit.)
Nit: remove this, and
@internalinstead :)We could do this as part of that same separate issue.
P.S.: I just posted https://github.com/json-api/json-api/issues/246#issuecomment-406747393 — pointing the JSON API spec contributors to Drupal core's solution. Hopefully it'll contribute to getting to consensus there!
Comment #43
btully commentedHey guys! Great progress on this. Just wanted to wanted to make you aware of a bug we experienced earlier today when testing jsonapi_file_upload.
On line 367 of src/Shim/FileUploader.php there is the following line:
which relies on the FILE_INSECURE_EXTENSION_REGEX constant. Unfortunately that constant is undefined in core < 8.6, and results in the following error:
So perhaps to be backwards compatible we need to check to see if the constant is defined, and if not define it?
Comment #44
gabesulliceThanks @btully! I was not aware. Great catch!
I think by the time this makes it into JSON API (in the 2.x branch) 8.6.0 will be the minimum supported version.
That was an oversight, but this issue got delayed long enough for it to not be a problem, oddly enough :)
Comment #45
grimreaperHello,
Thanks all for the work done here.
As it is impossible to apply patch from comment 40 on 2.0.0-beta1, I have rebased the patch against the last dev version.
Finally on a project using JSON API (but not Entity share...).
Comment #46
gabesulliceHello again! Thanks @Grimreaper!
Comment #47
grimreaperFlow 2 OK! \o/
Can't do Flow 1 because on the media entity the file field is processed and required.
Maybe see you guys at Drupal Europe next week!
Comment #48
gabesulliceThat's awesome.
Unfortunately, I don't think anyone from the team is gonna be there :(
Comment #49
grimreaperIs the PATCH/UPDATE of file entities supported?
PS: I have seen the work on the documentation pages. Great!
Comment #50
gabesulliceI'm not sure what you mean by that. Can you describe it in more detail?
Comment #51
grimreaperUsing the BO I think it is impossible.
But programmatically on the project I was for months, we had to do custom imports (absolutely not related to JSON API or webservices).
Depending on a file hierarchy with naming rules on a path on the server, if the client (or customer, not a client as in client/server :)) updated a file, if there was an already existing file entity in Drupal, we updated this file entity and we did not crreate a new one.
So I am asking, is it possible to update an existing file entity using JSON API? Or should a new file entity be created each time?
Comment #52
pfrillingI was testing this patch uploading image media and I came across this error message:
The curl command I was using:
The attached patch fixed the issue for me. Please review and let me know if something looks incorrect.
Comment #53
gabesullice@pfrilling, thanks! You're correct that it needs an array of links. However, it will need a
selflink. SeeEntityResource::buildWrappedResponse()for an example :)Comment #54
pfrillingThanks for the feedback @gabesullice! Attached is a new patch and interdiff. I think I did this correctly. Let me know if this looks okay.
Comment #55
pfrillingI forgot to mark this as needs review earlier.
Comment #59
gabesullicegetRequestLinkis not a static method. So the link manager service will need to be injected and called with an arrow->.Comment #60
pfrillingThanks again for the feedback.
Attached is a new patch. Hopefully I have it correct now.
Comment #61
gabesullice👌 looks great @pfrilling!
Would you like to try your hand at #42.5?
Comment #62
wim leers#43: Thanks for testing!
#44: I partially agree; I think that this particular feature should work only on Drupal 8.6 and newer, but I don't think we need to make all of JSON API 2.x 8.6-only. It'd complicate the upgrade path for sites; it's better if also sites on 8.5 can update to JSON API 2.x, because that means they are not required to update Drupal core to 8.6 to be able to use it, hence simplifying future updates!
#52 + #60: Thanks, @pfrilling! 🎉
Because this issue is blocking #2987608 as of #2987608-32: Move deserialization from RequestHandler to JsonApiParamEnhancer, I'm assigning this to me for a next round of work.
Comment #63
grimreaperHello,
I now have another use case. Due to the document size, files will be placed in the public/private stream wrapper folder by a process (not important). And I need to create the corresponding file entities.
I tried the following requests without success:
Returns a 403 forbidden, I think due to the permissions on file entity as there is no explicit "create" permission on the file entity.
And
Returns a 415 Unsupported media type.
Same result if I try on jsonapi/media/settlement/UUID/field_media_file endpoint.
So, it is no more possible to create a file entity without posting data?
Comment #64
gabesullice@Grimreaper, does your first example request work without this patch?
Comment #65
grimreaper@gabesullice,
Without the patch:
So it seems it was not possible even before the patch.
Comment #66
wim leers/jsonapi/file/file— you cannot create file entities directly, you need to upload them in the context of a specific file field. See IS and see https://www.drupal.org/node/2941420.POSTing not to a specific resource and you're still trying to create aFileentity (file--fileresource).It's not clear to me what you're trying to do. I think something else is already uploading the file, and now you're just trying to create a
Fileentity. Is that correct?Comment #67
grimreaperHello,
@Wim Leers thanks for the reply.
I am trying to create a file entity without file uplaod because the physical file has been already placed in the file system.
But if it can't be done, it is ok, I will find a solution for the project.
Another thing I realized, with either flows, it seems that it is impossible to create a file entity with a predefined UUID. Or I am missing something?
I think for Entity share, I will have problems with that, because currently on JSON API 1.x, using deserialization I can create file entities with predefined UUID, but with JSON API 2.x and the need to make subrequest, I don't know if it will still be possible. I will see when I will be confronted to the problem.
Comment #68
wim leersThis is definitely an edge case. How does the file get there then? There must be some other code making this happen, right? Can't that code then also create a
Fileentity for you?This is also not going possible in core REST's file upload support (see https://www.drupal.org/node/2941420). We'd need to create a new "move file already on server" endpoint for both core REST and JSON API to make this work.
Comment #69
grimreaperThe file is placed using a FTP connexion, there is no Drupal code involved in this process.
So yes, I think I will need to implement a custom endpoint to create those file entities.
Thanks for your replies.
Comment #70
gabesullice@Grimreaper, check out https://www.drupal.org/project/jsonrpc for that custom solution.
Comment #71
grimreaperThanks @gabesullice for the suggestion.
I will give it a look. I do not garantee to use it, but at least I will think more about it in the future.
Comment #72
grimreaperHello,
Rebasing patch from comment 60 to be able to apply it on version 8.x-2.0-beta2.
Edit: the only conflict was in the imports of jsonapi/src/Controller/EntityResource.php
Comment #73
wim leers#2997600: Resolve included resources prior to normalization and #2997277: Place all URLs under the `href` key landed last week, and require some changes.
Comment #74
wim leersThere are two key things that still need to happen for this patch:
rest.moduletooTackling those.
This does the first: test coverage (
interdiff-tests.txt). It unveiled a tiny bug in the existing patch (interdiff-logic_fix.txt). It's based on core's\Drupal\Tests\rest\Functional\FileUploadResourceTestBase, which was basically copy/pasted into\Drupal\Tests\jsonapi_file_upload\Functional\FileUploadTestand modified for JSON API. It's still 99% identical (seediff_rest_vs_jsonapi_test.txt).This should be green! 🎉
Comment #75
wim leersNote that #74 is only testing "flow #2" as the issue summary describes it: the request flow that requires two requests. This is also the flow that works in all cases. Flow #1 requires only a single request but only works in some cases. This flow still needs test coverage. But the test coverage in #74 already tests most things :)
Comment #77
wim leers🙃
Comment #78
gabesulliceI'm not sure I agree that this link should be the link for the individual file entity. Remember, the resource object under the
datakey will have its ownselflink that is the same as this one.This
selflink is on the document level and should be the URL of the requested resource.200 -> 201 | Good catch!
Comment #79
wim leers#77 passed on 8.7 and 8.6. On 8.5, this happened:
So, we should require Drupal >=8.6. New features for 8.5 make little sense anyway; 8.5 is on security-only support.
Comment #80
wim leersRunning #79 + #2987608-28: Move deserialization from RequestHandler to JsonApiParamEnhancer + #2987610-28: Remove RequestHandler class and service and add EntityResource methods to each route definition to answer my concern in #2987608-32: Move deserialization from RequestHandler to JsonApiParamEnhancer, where I wrote:
If this patch passes tests, then I am comfortable with marking #2987608: Move deserialization from RequestHandler to JsonApiParamEnhancer RTBC and committing it. Locally, it does pass tests:
So I'm expecting the same to happen here :)
Comment #81
wim leers🎉
Comment #82
wim leersUpdated IS with remaining tasks.
Comment #83
wim leersHaving been through this patch and this issue again today, I now also agree with the need for this to be a separate module for as long as the JSON API spec does not support file uploads.
@e0ipso, do you also agree?
Comment #84
wim leers#2987608: Move deserialization from RequestHandler to JsonApiParamEnhancer and #2987610: Remove RequestHandler class and service and add EntityResource methods to each route definition landed. Rebased. One tiny conflict.
Comment #85
e0ipsoI don't have a strong preference. I can see benefits one way or the other. My vote goes to whatever the majority think.
Comment #86
gabesulliceI do believe it should be a separate module which needs to be enabled. My preference is a to have it as a submodule as it already is in the patch. However, if @Wim Leers feels strongly that it should be in contrib, I'm fine w/ that too.
Comment #87
wim leersjsonapiwere to remain a contrib module, +1 for a submodule.jsonapigoes into core, it'll have to become a separate contrib module anyway. Core doesn't have submodules.Unless we're saying we want to add both modules to core. I can see that argument too.
Comment #88
gabesulliceCan core modules have submodules?
Comment #89
wim leersNot AFAIK. Hence why I wrote
Comment #90
gabesullice🙈
Okay... hm.
I would not like JSON API to be in core and have file uploads only supported by a contrib module.
That means our only choice would be between trying to get two modules into core, or to merge this into JSON API itself.
So... maybe we should make it part of the main module. Previously, I said:
That means I'm now arguing against myself. What's changed?
In some respects, this actually lives "outside" the spec, because the HTTP request
Content-Typeheader is not the JSON API media type. The HTTP response part of the upload flow is within the spec though, but the response is actually spec-compliant.Given those facts, I'm not too worried that we'll end up in conflict with the spec... only that we may end up competing with the spec if the spec maintainers come up with an alternative pattern (I think this is very unlikely). In that case, we'll have to implement both which is a poor DX.
Last point: the next version of the spec actually does have a negotiation method now (it didn't when I wrote that comment). That means that unlike before, we'd have a way to support two different file upload strategies if we wanted to.
Comment #91
wim leersThis is a very good point.
In that sense, this would be "just another" extension/profile that the Drupal implementation provides by default, just like we already have for filters, pagination and relationship arity.
Comment #95
wim leersIn preparation for #2940383: [META] Unify file upload logic of REST and JSON:API, let's reduce the JSON API-coupledness of the shim.
Comment #96
wim leersRationale for #95:
This was the only dependency on JSON API-specific logic.
This is what I replaced it with. We needed only a subset of the logic in that trait method!
Furthermore, rather than throwing an exception, I'm now instead mapping
file_validate()-returned errors into constraint violations, so that we can just return constraint violations, and let each respective HTTP API (JSON API, REST, GraphQL) decide how to convert those violations into errors/responses.Comment #97
wim leersStill green ✅
This then uses the initial step that #96.3 made, and takes it all the way.
Comment #98
wim leersGreat, still passing tests! This closely matches the core service proposed in #2940383-7: [META] Unify file upload logic of REST and JSON:API.
The only differences are:
Which means that
rest.moduletoois DONE!
That only leaves:
Comment #99
wim leersWorking on that last test coverage BTW :)
Comment #100
wim leersThis adds test coverage for flow #1. It should fail, due to 2 small bugs in
\Drupal\jsonapi_file_upload\Controller\RequestHandler.Comment #101
wim leersThis adds test coverage for flow #1. It should fail, due to 2 small bugs in
\Drupal\jsonapi_file_upload\Controller\RequestHandler.Comment #102
wim leersThe two small bugs:
'entity'/ for route parameter :)$serverparameter, which meant that the hostname was unknown, which resulted inhttp://localhostrather than the correct (current request's) hostname.This should be green.
Now all that remains:
Comment #103
wim leersOh, and that review should also settle/decide whether we want this to be a submodule, or whether we think this should be merged with the
jsonapimodule itself. I think that in #90 + #91 we agreed to move this intojsonapiitself?(Moving that is trivial, what matters most is complete test coverage, which we now have!)
Comment #105
gabesulliceYes, this should be moved into the main module.
Comment #106
wim leersComment #108
wim leers✅
Comment #110
wim leersHah. That one failure is because the
jsonapi.node--cat.file_upload.existing_resourceroute still is a route that on a fundamental level matches requests made to/jsonapi/node/cat/UUID/field_test, but it only supportsPOSTand hence results in a 405 response. Previously it resulted in a 404 response.I think that changing this logic to generate routes specific to each file field that exists would solve this. That'd also mean that we don't generate such file upload routes for entity types that don't even have file fields (which is usually most of them).
Thoughts?
Comment #111
gabesullice@Wim Leers, you're absolutely right! The current code is just vestige of the past and a reminder how long this issue has been open.
Back in July, @malik.kotob and I had this conversation:
The issue I linked to was: #2953346: Define related/relationship routes per field, not dynamically (with route parameters that need validating)
And it has already landed! So it's time to do that here as well :)
Comment #112
wim leers👌
Comment #113
wim leersDone. Unfortunately new edge cases arise. For example, whenPOST /jsonapi/entity_test/entity_test/field_rest_file_test_INVALID(notice the suffix), it'll result in a 405, not in a 404. Because/jsonapi/entity_test/entity_test/{WILDCARD}no longer exists, and that's what used to validate … oh wait 🤔That's because I specifically was generating a different route per file field. Only generating a single route if >=1 file field exists on a resource type is sufficient. Then the exact same approach as #2953346: Define related/relationship routes per field, not dynamically (with route parameters that need validating) can be used! 👌
Comment #114
wim leersRebased now that #3007274: s/JSON API/JSON:API/ landed.
Comment #116
wim leersMade a mistake in rebasing. Also explains the difference in patch size between #113 and #114.
Comment #117
gabesulliceLet's use
iterator_to_array($violations)here. Technically,getIterator()isn't part of the interface.Nit:
s/-//gUbernit:
s/Firstly/FirstNit: need trailing stops (
.)Nit:
s/-//Nit:
s/users/user's/Remove: "Defaults to the current user" since it's not an optional arg.
Only nits... and they're all fixed! RTBC!
But let's not commit this until 2.0 stable. Also, still needs a CR.
Comment #118
wim leers+1!
Can you also apply these changes to the core patch at #2940383: [META] Unify file upload logic of REST and JSON:API?
Interesting. I thought it was "The foo level" and "A foo-level something".
I'd love to learn what's right. Can you point me to some URLs? :)
Comment #119
gabesullice#118:
So, "field level access" should be "field-level access", but "implemented at the field level" is correct, because "field" is modifying "level".
Sources: https://www.grammarbook.com/punctuation/hyphens.asp and https://english.stackexchange.com/questions/113422/how-to-use-hyphens-ap...
I added a "corrected correction" interdiff which shows just the hyphen changes to #116.
TIL: @Wim Leers is better and englishing than me.
Comment #120
awm commentedI applied this patch and tried to test it but it did not work for me: Here is my post request exported as curl from postman
and request body is a binary file.
The response I get is
But nothing gets attached to article with uuid 9cff7586-deb8-4f5f-8fe6-dc05cf839ec0. Am I missing anything?
I tried adding the accept header but as when I do I get a message "Could not get any response"
Comment #121
wim leersFirst of all: thanks for testing this patch! 🎉
Hm … it sounds like you've found a bug either way: either something is broken, or some validation of the incoming request is insufficient. Either way, we need to expand test coverage for this issue it seems. Unless you're using another module that's causing this problem. Can you reproduce this with only Drupal core + JSON:API?
Because the
testPostFileUploadAndUseInSingleRequest()test is passing and it does this:This is doing exactly what you're doing. I'd set a
global $something = TRUE;here, and then modify thefileRequest()method to check the value of that same global, and then dump/log what exactly its request is doing. Once you have that, you can compare it to what's happening for you.Comment #122
awm commentedOk tested this again on vannila drupal "8.6.4-dev " + jsonapi "8.x-2.0-rc2 " with the patch applied. here is my command line curl request:
note: cookie, csrf token, and path to file must be obtained and changed before you can try this your
The result I get in apache access log is
"POST /jsonapi/node/article/307a5965-6a20-4f19-a928-2663a3e05d9a/field_image HTTP/1.1" 400 - "-" "curl/7.54.0"and
in the error log I get;
[Wed Dec 05 17:12:06.429200 2018] [php7:notice] [pid 1404] [client 192.168.44.1:49995] Uncaught PHP Exception InvalidArgumentException: "Class "jsonapi.file_upload" does not exist." at /var/www/d8/web/core/lib/Drupal/Core/DependencyInjection/ClassResolver.php line 24Comment #123
awm commentedTested this again and it looks like content-despostion header is requried otherwise I get a 400 bad request.
Here is what worked for me on vanilla drupal core + jsonapi + patch:
Comment #124
blainelang commentedI am successfully uploading files from REACT app via the /file/upload endpoint and attaching it to the content node using the /jsonapi/node/[entity_type]/[entity_uuid]/relationships/[field_name] end point without this patch. I'm confused then as to what this patch is going to be doing. I thought it was adding the binary file upload capability - but this is working with out any patches for me. I've tested with different file types and sizes < 100Meg including MP4 video files.
Just did a clean test on a new install so I could be certain this was all that was needed - see attached image of the modules and versions installed.

Comment #125
wim leers@awm: Correct, the
Content-Dispositionheader is required. The error response's body will inform you about that. The error in #122 is because you applied the patch without rebuilding the container, hence it was still missing the service. Thanks for confirming in #123 that it is working correctly for you! 👍@blainelang
That's because you're using core's
restmodule and have enabled the file upload REST resource.This patch is bringing that same capability natively to the JSON:API module, so that you don't need to enable the REST module at all, nor the REST UI module, nor any of the fairly confusing configuration that you need to do to be able to use it at all.
Comment #126
blainelang commentedThanks @Win for clarifying!
Comment #127
hedeshy commentedHi everybody,
In jsonapi_file it is possible to set the file UUID along with the file data. How to do it in this approach?
I mean, maybe we can have another flow for such a case.
Flow 3 (creating a new entity with defined uuid)
Request:
Comment #128
gabesullice@hedeshy, can you open a feature request like this:
[PP-1] Support client-generated UUIDs on file uploadsand then mark it Postponed and related to this issue? I don't think that that feature needs to be in this patch.Regarding your proposal, I think
GETparameters would be a more fitting solution than a path parameter. So/jsonapi/entity_type/bundle/file_field/{uuid}would become/jsonapi/entity_type/bundle/file_field?id={uuid}. OTOH, it would be worth researching theContent-Dispositionheader to see if it could support a UUID, since that is already the mechanism in place for setting the file's metadata.EDIT: I did a (very quick) bit of research. It looks like the ABNF for
Content-Dispositionparameters would allow for setting the file entity's UUID, like so:Comment #129
andriansyah commentedHi,
Thank you for the #119 patch, but after the upgrade to the 8.x-2.0-rc4, I found this error.

Comment #130
andriansyah commentedafter doing some diff and merge using Diffmerge.
this is the patch that I used after updating to 8.x-2.0-rc4.
Comment #131
andriansyah commentedComment #134
wim leersThanks, @andriansyahnc! That looks perfect :) (I rebased it myself, fixed the trivial conflict at the top of
EntityResource, and ended up with the same diff, only with different contexts.)Comment #135
wim leersRemoving again — I added it in #121 based on #120, but based on the response in #122, that wasn't an actual bug/problem.
Comment #136
wim leershttps://www.drupal.org/project/jsonapi/releases/8.x-2.0 is out. We now actually can commit this! Change record created: https://www.drupal.org/node/3024331
Documentation can only be added after this is committed. The page already exists: https://www.drupal.org/docs/8/modules/jsonapi/file-uploads. I think adding the same content as the change record upon commit is a good start?
Comment #137
gabesulliceThis patch fails on 8.5 (results) and 8.7 (results)
Comment #138
gabesulliceThis should fix this for 8.7 and 8.6 (I haven't tested it on 8.5, perhaps it will help there too). Also, I added a missing piece of a docblock and
Accept: application/vnd.api+jsontoFileUploadTest::fileRequest().Comment #139
gabesulliceIn #79, we decided not to support file uploads on Drupal core < 8.6. Typically, we support 8.5 and above on the 2.x branch. However, this is a completely new feature, so we're not dropping support for 8.5.
We didn't previously have a problem with this because we had all this in a submodule which required >8.6 in its dependencies.
I wrote a bunch of version checks and skipped tests if < 8.6 is detected. I also changed the version detection mechanism in
Routes.phpfrom this:to this:
That's what we do everywhere else. What this check does is prevent any file upload routes from being generated on 8.5 and below.
IMHO, if @Wim Leers or @e0ipso agrees, this can be committed.
Comment #140
wim leers#138: ugh, yeah, that's caused by upstream Symfony weirdness. We've had to make similar changes all over core. Except … #2940383: [META] Unify file upload logic of REST and JSON:API does pass on Drupal 8.7. And this patch includes a forward ported bit from there … so I don't like deviating from that class unless we have very good and documented reasons. Investigating…
In the mean time, here's a rebase, because this no longer applied since #2992833: Add a version negotiation to revisionable resource types got committed.
Comment #141
wim leersInstead of doing this for every
test*()method, we can just add a single@requiresin the test class' docblock.Comment #142
wim leersAnd the changes that #138 made to the
FileUploaderservice have been confirmed to be unnecessary after a painful digging deeper (made simpler by @gabesullice!): https://github.com/guzzle/guzzle/issues/1882 changed an implementation detail in Guzzle 6.3.2, and 8.7 updated to 6.3.3 in #3001542: Update Guzzle from 6.3.0 to 6.3.3, where they updated the sibling test method\Drupal\Tests\rest\Functional\FileUploadResourceTestBase::testPostFileUploadInvalidHeaders()to accommodate that change.IOW: core updated its dependencies, which broke the test, which had been relying on a bug in Guzzle… 😵
So this interdiff reverts a portion of #138 and ports a portion of #3001542. Which means the
FileUploaderclass is now no longer being customized.Comment #143
wim leersSee #2940383-16: [META] Unify file upload logic of REST and JSON:API.
Which makes me wonder whether we should commit this to JSON:API 2.1. It introduces an extra blocker for JSON:API to get into core…
Comment #144
stillfinder commentedHello everybody,
I'm trying to apply the patch https://www.drupal.org/files/issues/2019-01-08/2958554-139.patch , but when I run composer update, I get: "Could not apply patch! Skipping. The error was: Cannot apply patch https://www.drupal.org/files/issues/2019-01-08/2958554-139.patch". Am I doing anything wrong?
Thanks for any help!
P.S.: I've added the following lines to my composer.json:
Comment #145
wim leers#144: Thanks for testing this new feature! 🙏 👍
8.x-2.0.8.x-2.xHEAD, which is currentlyad50c207ce849b1a37946aa40073708eb340eb2d.Comment #146
stillfinder commented#145: Thank you for your response, but when I'm trying #140 I get the same error:
Could not apply patch! Skipping. The error was: Cannot apply patch https://www.drupal.org/files/issues/2019-01-09/2958554-140.patch
Added to composer:
Maybe I need another patch for my Drupal Version 8.5.7 ?
Also, my JSON API Version is 8.x-2.0-rc1
Comment #147
wim leersThis is your problem. RC1 is ancient. 2.0 was released on Monday. Update to that, and then use #139. That will work :)
Comment #148
stillfinder commented#147 Thanks a lot for your answer, it helped to apply #139 patch. But I have faced with another problem - I'm getting 405 Method Not Allowed in response.
I have the user_picture(machine name) field, with the field type: Image. Maybe I miss something in my request. Or maybe it's because of my Drupal Version 8.5.7 ?
Request:
POST {{url}}/jsonapi/user/user/4d861a6a-8f69-45e5-a06b-9fd25b207cb9/user_picture
Content-Type: application/octet-stream
Accept: application/vnd.api+json
X-CSRF-Token:fPvqqR4UHrK_YuGO2oYHLkW-kKefmZMzuHqMGG0uYCI
cache-control: no-cache
Body: binary
Response
Thank you for any help.
Comment #149
gabesullice@stillfinder, see comment #139 where we say that Drupal 8.5 is not supported. You'll have to update to Drupal 8.6. Running uncommitted patches can be a bit of a pain. If you get it working, we'd like to know how it works for you :)
Comment #150
stillfinder commented#149 Thank you! It works with 8.6.
But, is there any older patch that works with Drupal 5.8 ?
Comment #151
wim leers@stillfinder: No. This needs certain improvements in Drupal 8.5. Also, 8.5 only has a few more months of security support left. There's no good reason to build something new on 8.5 today. Any development you do should be on 8.6.
Comment #152
stillfinder commented@Wim Leers - Thank you for your response!
Comment #153
stillfinder commentedHi, I've found that after update the jsonapi from 2.0-rc1 to 2.0 and apply the patch #139 the following request {{url}}/jsonapi/user/user?filter[uid]=1&fields[user--user]=name returns the next:
but before, the response was:
Comment #154
wim leersThat is 100% unrelated to this issue. Please open a separate issue for that. Thanks! 🙏
Comment #155
stillfinder commented@Wim Leers thank you, I'll do
Comment #156
wim leersThere's one more reroll over at #2940383-20: [META] Unify file upload logic of REST and JSON:API.
Comment #158
wim leersOops. Incomplete patch.
Comment #159
wim leersGreen again on all three supported core branches, so back to RTBC.
Comment #161
wim leersBased on feedback in chat with the other JSON:API maintainers:
And given that so many people seem to be applying and using this patch in production: #43 + #47 + #52 + #120 + #124 + #127 + #129 + #144 means at least 8 people are using this today, and we know for sure this is an underestimation.
Given maintainer support and strong demand, I think that outweighs my concern in #143. So let's do this!
Comment #162
steveworley commentedHi I recently included this patch in a site I'm working on and am currently running into issues, I was wondering if anyone might be able to point me in the right direction.
I'm generating a CSRF token first with /rest/session/token
Then I'm sending a POST request to the file field which looks like:
The user that I'm trying it with is an administrator and has all permissions. The error I'm getting is:
The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: Drupal\\jsonapi\\ResourceResponse.I'm sure I've just missed something.
One other question I had was - it looks that the field has to be "file" in Drupal and image fields are not supported. Just wondering if I missed something there as well.
Thanks for your help!
Comment #163
wim leersCan you please open a new issue for that? Thanks! 🙏
Comment #164
wim leers@steveworley opened #3026459: The controller result claims to be providing relevant cache metadata, but leaked metadata was detected and the problem he was seeing disappeared as he updated to the latest commit in the branch.
Also, the upstream #2940383: [META] Unify file upload logic of REST and JSON:API that is included here was updated based on reviews, see #2940383-23: [META] Unify file upload logic of REST and JSON:API. Let's update the committed forward compatibility layer to match that latest iteration. It also fixes a problem on environments with assertions turned on (all dev environments by default) where it'd forbid uploads to e.g. image fields due to an incorrect assertion.
Comment #166
wim leers#164 was green on 8.5+8.6+8.7. Committed.
Comment #168
shenzhuxi commentedIs there anyone successfully create a file entity by POSTING file to file/file endpoint or update an existing file entity by PATCHING to file/file/uuid endpoint?
With "Content-Type: application/octet-stream", I got code "415"/error "Unsupported Media Type" for both POST and PATCH methods (8.x-2.x+core 8.6.x).
Server side log:
Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException: No route found that matches "Content-Type: application/octet-stream" in Drupal\Core\Routing\ContentTypeHeaderMatcher->filter() (line 49 of drupal8/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php).
Comment #169
Tronux commentedStill having this issue in D8.8
Request
Content-Type | application/octet-stream
Content-Disposition | file; filename="Testing1.jpg"
Response: 415Unsupported Media Type
Comment #170
rpayanmMoving to Drupal core's issue queue.
I'm working on https://www.drupal.org/project/drupal/issues/3122113