<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Javascript on Lessons learned</title><link>https://eghuro.cz/tags/javascript/</link><description>Recent content in Javascript on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Sun, 19 Apr 2026 00:17:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/javascript/index.xml" rel="self" type="application/rss+xml"/><item><title>Google Drive drive.file + Picker: picking a folder grants the folder, not its contents</title><link>https://eghuro.cz/posts/google-drive-file-picker-folder-empty-list/</link><pubDate>Sun, 19 Apr 2026 00:17:00 +0200</pubDate><guid>https://eghuro.cz/posts/google-drive-file-picker-folder-empty-list/</guid><description>&lt;p&gt;A web app lets the user pick a Google Drive folder in the Google Picker, then lists that folder from the server with the Drive API v3. The folder was full of files. The listing came back like this, every time:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;files&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:[]}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The status was 200, with no 403 and nothing in the response that mentioned permissions.&lt;/p&gt;&#10;&lt;p&gt;If you request the &lt;code&gt;drive.file&lt;/code&gt; scope and let users pick a folder, this is expected: the grant covers the folder itself and none of the files inside it.&lt;/p&gt;&#10;&#10;&lt;h2 id="setup"&gt;Setup &lt;/h2&gt;&#10;&lt;p&gt;The browser gets an OAuth access token with the &lt;code&gt;drive.file&lt;/code&gt; scope and opens the Picker on a folder view:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;picker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DocsView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;picker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ViewId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FOLDERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setMimeTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;application/vnd.google-apps.folder&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setSelectFolderEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setIncludeFolders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;picker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;picker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PickerBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setOAuthToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setDeveloperKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onPicked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The callback sends the picked folder ID to the server, which walks the folder with &lt;code&gt;files.list&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://www.googleapis.com/drive/v3/files&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;folderId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;&amp;#39; in parents and trashed = false`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fields&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;nextPageToken, files(id, name, mimeType, size)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;supportsAllDrives&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;includeItemsFromAllDrives&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;drive.file&lt;/code&gt; looks like the obvious scope here. The &lt;a&#10; href="https://developers.google.com/workspace/drive/picker/guides/web-picker"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Picker guide&lt;/a&gt;&#10; tells you to use it, and Google&amp;rsquo;s &lt;a&#10; href="https://developers.google.com/workspace/drive/api/guides/api-specific-auth"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;scope list&lt;/a&gt;&#10; marks it non-sensitive, so a public app needs only basic verification. The app only ever reads folders the user picked, which sounds like exactly what the scope is for.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-dead-end-setappid"&gt;The dead end: setAppId &lt;/h2&gt;&#10;&lt;p&gt;The first suspect was &lt;code&gt;PickerBuilder.setAppId()&lt;/code&gt;. The setup above doesn&amp;rsquo;t call it, and the Picker guide says to &amp;ldquo;set the Drive App ID using the Cloud project number to allow the app to access the user&amp;rsquo;s files&amp;rdquo;. The theory was that without an app ID, the grant belongs to the access token that was live at pick time, so the server&amp;rsquo;s later call with a refreshed token sees nothing.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAppId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CLOUD_PROJECT_NUMBER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// the numeric project number, not the project ID&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you stay on &lt;code&gt;drive.file&lt;/code&gt;, set it, because the guide asks for it. It didn&amp;rsquo;t change the result here. The listing was still &lt;code&gt;{&amp;quot;files&amp;quot;:[]}&lt;/code&gt;.&lt;/p&gt;&#10;&#10;&lt;h2 id="whats-actually-going-on"&gt;What&amp;rsquo;s actually going on &lt;/h2&gt;&#10;&lt;p&gt;To see where the files went, the server logged the raw body of every &lt;code&gt;files.list&lt;/code&gt; response. A truly empty folder and a folder the app can&amp;rsquo;t see into give the same answer: status 200 and &lt;code&gt;&amp;quot;files&amp;quot;: []&lt;/code&gt;. The app only had access to the selected folder. Its children were never shared, so Drive left them out of the result and didn&amp;rsquo;t report an error.&lt;/p&gt;&#10;&lt;p&gt;The scope&amp;rsquo;s description says this once you read it with folders in mind. &lt;code&gt;drive.file&lt;/code&gt; covers files &amp;ldquo;that you open with an app or that the user shares with an app&amp;rdquo; — picking a folder shares one item: the folder. The docs never say the grant extends to its contents, and it doesn&amp;rsquo;t.&lt;/p&gt;&#10;&#10;&lt;h2 id="fix"&gt;Fix &lt;/h2&gt;&#10;&lt;p&gt;Pick the scope that matches what the server does. A server that reads everything under the picked folder can&amp;rsquo;t do that with &lt;code&gt;drive.file&lt;/code&gt;, so the app switched to &lt;code&gt;drive.readonly&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DRIVE_SCOPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://www.googleapis.com/auth/drive.readonly&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It kept the Picker for choosing the folder and removed the &lt;code&gt;setAppId&lt;/code&gt; call, which &lt;code&gt;drive.readonly&lt;/code&gt; doesn&amp;rsquo;t need.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;drive.readonly&lt;/code&gt; is a &lt;a&#10; href="https://developers.google.com/workspace/drive/api/guides/api-specific-auth"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;restricted scope&lt;/a&gt;&#10;, so a public app needs Google&amp;rsquo;s restricted-scope verification to use it. The app in this story was internal to one Google Workspace domain, so verification didn&amp;rsquo;t apply.&lt;/p&gt;&#10;&lt;p&gt;Update the consent text in your own UI as well. &amp;ldquo;The app only sees folders you pick&amp;rdquo; was true under &lt;code&gt;drive.file&lt;/code&gt; and false after the switch. The new text says the app has read-only access to the whole Drive and downloads only from the folders the user picks.&lt;/p&gt;&#10;&lt;p&gt;Two other ways out may fit your constraints better. If users can pick files instead of a folder, enable multi-select in the Picker: every picked file is shared with the app, and &lt;code&gt;drive.file&lt;/code&gt; covers it. Or use a service account and have users share the folder with its address, which moves the access decision into Drive&amp;rsquo;s own sharing dialog. We ruled that out here for organizational reasons.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;Under &lt;code&gt;drive.file&lt;/code&gt;, the Picker shares exactly the items the user selects. A folder is one item, and its children aren&amp;rsquo;t part of the grant, so listing it returns an empty &lt;code&gt;files&lt;/code&gt; array with status 200.&lt;/p&gt;&#10;&lt;p&gt;If your app requests &lt;code&gt;drive.file&lt;/code&gt; and calls &lt;code&gt;setSelectFolderEnabled(true)&lt;/code&gt;, list a folder you know isn&amp;rsquo;t empty from the server and log the raw response before you ship.&lt;/p&gt;&#10;</description></item></channel></rss>